diff --git a/lib/src/app.dart b/lib/src/app.dart index aa4969772..298234efd 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -62,10 +62,12 @@ class AppConfiguration { /// These can be the same conceptual app developed for different platforms, or /// significantly different client side applications that operate on the same data - e.g. an event managing /// service that has different clients apps for organizers and attendees. + @Deprecated("localAppName is not used.") final String? localAppName; /// The [localAppVersion] can be specified, if you wish to distinguish different client versions of the /// same application. + @Deprecated("localAppVersion is not used.") final String? localAppVersion; /// Enumeration that specifies how and if logged-in User objects are persisted across application launches. diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index cd63401d3..3c82167ad 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -614,32 +614,26 @@ class ClientResetError extends SyncError { /// The [ClientResetError] has error code of [SyncClientErrorCode.autoClientResetFailure] /// when a client reset fails and `onManualResetFallback` occurs. Otherwise, it is [SyncClientErrorCode.unknown] + @Deprecated("Use errorCode property") SyncClientErrorCode get code => SyncClientErrorCode.fromInt(codeValue); /// The [SyncSessionErrorCode] value indicating the type of the sync error. /// This property will be [SyncSessionErrorCode.unknown] if `onManualResetFallback` occurs on client reset. + @Deprecated("Use errorCode property") SyncSessionErrorCode get sessionErrorCode => SyncSessionErrorCode.fromInt(codeValue); - @Deprecated("ClientResetError constructor is deprecated and will be removed in the future") - ClientResetError( + ClientResetError._( String message, { App? app, - SyncErrorCategory category = SyncErrorCategory.client, int? errorCodeValue, this.backupFilePath, this.originalFilePath, - String? detailedMessage, }) : _app = app, - super( - message, - category, - errorCodeValue ?? SyncClientErrorCode.autoClientResetFailure.code, - detailedMessage: detailedMessage, - ); + super._(message, SyncErrorCode.autoClientResetFailure); @override String toString() { - return "ClientResetError message: $message category: $category code: $code isFatal: $isFatal"; + return "ClientResetError message: $message code: $errorCode isFatal: $isFatal"; } /// Initiates the client reset process. @@ -660,52 +654,34 @@ class ClientResetError extends SyncError { /// {@category Sync} class SyncError extends RealmError { /// The numeric code value indicating the type of the sync error. + @Deprecated("Use property code") final int codeValue; + /// If true the received error is fatal. + final bool isFatal = false; + /// Type of the sync error. + final SyncErrorCode errorCode; + /// The category of the sync error - final SyncErrorCategory category; - - /// Detailed error message. - /// In case of server error, it contains the link to the server log. - final String? detailedMessage; - - @Deprecated("SyncError constructor is deprecated and will be removed in the future") - SyncError(String message, this.category, this.codeValue, {this.detailedMessage}) : super(message); - - /// Creates a specific type of [SyncError] instance based on the [category] and the [code] supplied. - @Deprecated("This method is deprecated and will be removed in the future") - static SyncError create(String message, SyncErrorCategory category, int code, {bool isFatal = false}) { - switch (category) { - case SyncErrorCategory.client: - final SyncClientErrorCode errorCode = SyncClientErrorCode.fromInt(code); - if (errorCode == SyncClientErrorCode.autoClientResetFailure) { - return ClientResetError(message); - } - return SyncClientError(message, category, errorCode, isFatal: isFatal); - case SyncErrorCategory.connection: - return SyncConnectionError(message, category, SyncConnectionErrorCode.fromInt(code), isFatal: isFatal); - case SyncErrorCategory.session: - return SyncSessionError(message, category, SyncSessionErrorCode.fromInt(code), isFatal: isFatal); - case SyncErrorCategory.webSocket: - return SyncWebSocketError(message, category, SyncWebSocketErrorCode.fromInt(code)); - case SyncErrorCategory.system: - case SyncErrorCategory.unknown: - default: - return GeneralSyncError(message, category, code); - } - } + @Deprecated("No sync error categories") + final SyncErrorCategory category = SyncErrorCategory.system; + + SyncError._(String message, this.errorCode) + : codeValue = errorCode.code, + super(message); /// As a specific [SyncError] type. T as() => this as T; @override String toString() { - return "SyncError message: $message category: $category code: $codeValue"; + return "SyncError message: $message code: $errorCode"; } } /// An error type that describes a session-level error condition. /// {@category Sync} +@Deprecated("Use SyncError") class SyncClientError extends SyncError { /// If true the received error is fatal. final bool isFatal; @@ -718,9 +694,8 @@ class SyncClientError extends SyncError { String message, SyncErrorCategory category, SyncClientErrorCode errorCode, { - String? detailedMessage, this.isFatal = false, - }) : super(message, category, errorCode.code, detailedMessage: detailedMessage); + }) : super._(message, SyncErrorCode.unknown); @override String toString() { @@ -730,6 +705,7 @@ class SyncClientError extends SyncError { /// An error type that describes a connection-level error condition. /// {@category Sync} +@Deprecated("Use SyncError") class SyncConnectionError extends SyncError { /// If true the received error is fatal. final bool isFatal; @@ -742,9 +718,8 @@ class SyncConnectionError extends SyncError { String message, SyncErrorCategory category, SyncConnectionErrorCode errorCode, { - String? detailedMessage, this.isFatal = false, - }) : super(message, category, errorCode.code, detailedMessage: detailedMessage); + }) : super._(message, SyncErrorCode.unknown); @override String toString() { @@ -754,6 +729,7 @@ class SyncConnectionError extends SyncError { /// An error type that describes a session-level error condition. /// {@category Sync} +@Deprecated("Use SyncError") class SyncSessionError extends SyncError { /// If true the received error is fatal. final bool isFatal; @@ -766,9 +742,8 @@ class SyncSessionError extends SyncError { String message, SyncErrorCategory category, SyncSessionErrorCode errorCode, { - String? detailedMessage, this.isFatal = false, - }) : super(message, category, errorCode.code, detailedMessage: detailedMessage); + }) : super._(message, SyncErrorCode.unknown); @override String toString() { @@ -776,39 +751,16 @@ class SyncSessionError extends SyncError { } } -/// Network resolution error -/// -/// This class is deprecated and it will be removed. The sync errors caused by network resolution problems -/// will be received as [SyncWebSocketError]. -@Deprecated("Use SyncWebSocketError instead") -class SyncResolveError extends SyncError { - /// The numeric value indicating the type of the network resolution sync error. - SyncResolveErrorCode get code => SyncResolveErrorCode.fromInt(codeValue); - - SyncResolveError( - String message, - SyncErrorCategory category, - SyncResolveErrorCode errorCode, - ) : super(message, category, errorCode.index); - - @override - String toString() { - return "SyncResolveError message: $message category: $category code: $code"; - } -} - /// Web socket error +@Deprecated("Use WebSocketError") class SyncWebSocketError extends SyncError { /// The numeric value indicating the type of the web socket error. SyncWebSocketErrorCode get code => SyncWebSocketErrorCode.fromInt(codeValue); - @Deprecated("SyncWebSocketError constructor is deprecated and will be removed in the future") - SyncWebSocketError( + SyncWebSocketError._( String message, - SyncErrorCategory category, - SyncWebSocketErrorCode errorCode, { - String? detailedMessage, - }) : super(message, category, errorCode.code, detailedMessage: detailedMessage); + SyncWebSocketErrorCode errorCode, + ) : super._(message, SyncErrorCode.unknown); @override String toString() { @@ -817,17 +769,15 @@ class SyncWebSocketError extends SyncError { } /// A general or unknown sync error +@Deprecated("Use SyncError") class GeneralSyncError extends SyncError { /// The numeric value indicating the type of the general sync error. int get code => codeValue; - @Deprecated("GeneralSyncError constructor is deprecated and will be removed in the future") - GeneralSyncError( + GeneralSyncError._( String message, - SyncErrorCategory category, - int code, { - String? detailedMessage, - }) : super(message, category, code, detailedMessage: detailedMessage); + int code, + ) : super._(message, SyncErrorCode.unknown); @override String toString() { @@ -836,6 +786,7 @@ class GeneralSyncError extends SyncError { } /// General sync error codes +@Deprecated("Use SyncErrorCode") enum GeneralSyncErrorCode { /// Unknown Sync error code unknown(9999); @@ -875,7 +826,9 @@ class CompensatingWriteInfo { /// by the server. /// {@category Sync} class CompensatingWriteError extends SyncError { + /// The [CompensatingWriteError] has error code of [SyncSessionErrorCode.compensatingWrite] + @Deprecated("Use errorCode property instead") SyncSessionErrorCode get code => SyncSessionErrorCode.compensatingWrite; /// The list of the compensating writes performed by the server. @@ -883,50 +836,26 @@ class CompensatingWriteError extends SyncError { CompensatingWriteError._( String message, { - String? detailedMessage, this.compensatingWrites, - }) : super(message, SyncErrorCategory.session, SyncSessionErrorCode.compensatingWrite.code, detailedMessage: detailedMessage); + }) : super._(message, SyncErrorCode.compensatingWrite); @override String toString() { - return "CompensatingWriteError message: $message category: $category code: $code. ${compensatingWrites ?? ''}"; + return "CompensatingWriteError message: $message code: $errorCode. ${compensatingWrites ?? ''}"; } } /// @nodoc extension SyncErrorInternal on SyncError { static SyncError createSyncError(SyncErrorDetails error, {App? app}) { + final errorCode = SyncErrorCode.fromInt(error.code); if (error.isClientResetRequested) { //Client reset can be requested with isClientResetRequested disregarding the SyncClientErrorCode and SyncSessionErrorCode values - return ClientResetError(error.message, - app: app, - category: error.category, - errorCodeValue: error.code, - originalFilePath: error.originalFilePath, - backupFilePath: error.backupFilePath, - detailedMessage: error.detailedMessage); - } - - switch (error.category) { - case SyncErrorCategory.client: - final errorCode = SyncClientErrorCode.fromInt(error.code); - return SyncClientError(error.message, error.category, errorCode, detailedMessage: error.detailedMessage, isFatal: error.isFatal); - case SyncErrorCategory.connection: - final errorCode = SyncConnectionErrorCode.fromInt(error.code); - return SyncConnectionError(error.message, error.category, errorCode, detailedMessage: error.detailedMessage, isFatal: error.isFatal); - case SyncErrorCategory.session: - final errorCode = SyncSessionErrorCode.fromInt(error.code); - if (errorCode == SyncSessionErrorCode.compensatingWrite) { - return CompensatingWriteError._(error.message, detailedMessage: error.detailedMessage, compensatingWrites: error.compensatingWrites); - } - return SyncSessionError(error.message, error.category, errorCode, detailedMessage: error.detailedMessage, isFatal: error.isFatal); - case SyncErrorCategory.webSocket: - final errorCode = SyncWebSocketErrorCode.fromInt(error.code); - return SyncWebSocketError(error.message, error.category, errorCode, detailedMessage: error.detailedMessage); - case SyncErrorCategory.system: - case SyncErrorCategory.unknown: - default: - return GeneralSyncError(error.message, error.category, error.code, detailedMessage: error.detailedMessage); + return ClientResetError._(error.message, + app: app, errorCodeValue: error.code, originalFilePath: error.originalFilePath, backupFilePath: error.backupFilePath); + } else if (errorCode == SyncErrorCode.compensatingWrite) { + return CompensatingWriteError._(error.message, compensatingWrites: error.compensatingWrites); } + return SyncError._(error.message, errorCode); } } diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index d0a0a61ad..646cd8dfe 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -326,45 +326,6 @@ class RealmLibrary { void Function( ffi.Pointer, ffi.Pointer)>(); - void realm_app_config_set_local_app_name( - ffi.Pointer arg0, - ffi.Pointer arg1, - ) { - return _realm_app_config_set_local_app_name( - arg0, - arg1, - ); - } - - late final _realm_app_config_set_local_app_namePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('realm_app_config_set_local_app_name'); - late final _realm_app_config_set_local_app_name = - _realm_app_config_set_local_app_namePtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void realm_app_config_set_local_app_version( - ffi.Pointer arg0, - ffi.Pointer arg1, - ) { - return _realm_app_config_set_local_app_version( - arg0, - arg1, - ); - } - - late final _realm_app_config_set_local_app_versionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.Pointer)>>( - 'realm_app_config_set_local_app_version'); - late final _realm_app_config_set_local_app_version = - _realm_app_config_set_local_app_versionPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - void realm_app_config_set_platform_version( ffi.Pointer arg0, ffi.Pointer arg1, @@ -3834,7 +3795,7 @@ class RealmLibrary { void realm_dart_sync_wait_for_completion_callback( ffi.Pointer userdata, - ffi.Pointer error, + ffi.Pointer error, ) { return _realm_dart_sync_wait_for_completion_callback( userdata, @@ -3844,13 +3805,12 @@ class RealmLibrary { late final _realm_dart_sync_wait_for_completion_callbackPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>( + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>( 'realm_dart_sync_wait_for_completion_callback'); late final _realm_dart_sync_wait_for_completion_callback = _realm_dart_sync_wait_for_completion_callbackPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); + void Function(ffi.Pointer, ffi.Pointer)>(); void realm_dart_userdata_async_free( ffi.Pointer userdata, @@ -4768,8 +4728,9 @@ class RealmLibrary { /// /// @param err A pointer to a `realm_error_t` struct that will be populated with /// information about the error. May not be NULL. + /// @return A bool indicating whether or not an error is available to be returned /// @see realm_get_last_error() - void realm_get_async_error( + bool realm_get_async_error( ffi.Pointer err, ffi.Pointer out_err, ) { @@ -4781,10 +4742,10 @@ class RealmLibrary { late final _realm_get_async_errorPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, + ffi.Bool Function(ffi.Pointer, ffi.Pointer)>>('realm_get_async_error'); late final _realm_get_async_error = _realm_get_async_errorPtr.asFunction< - void Function( + bool Function( ffi.Pointer, ffi.Pointer)>(); /// Fetch the backlinks for the object passed as argument. @@ -9687,22 +9648,19 @@ class RealmLibrary { /// Wrapper for SyncSession::OnlyForTesting::handle_error. This routine should be used only for testing. /// @param session ptr to a valid sync session - /// @param error_code error code to simulate - /// @param category category of the error to simulate - /// @param error_message string representing the error + /// @param error_code realm_errno_e representing the error to simulate + /// @param error_str error message to be included with Status /// @param is_fatal boolean to signal if the error is fatal or not void realm_sync_session_handle_error_for_testing( ffi.Pointer session, int error_code, - int category, - ffi.Pointer error_message, + ffi.Pointer error_str, bool is_fatal, ) { return _realm_sync_session_handle_error_for_testing( session, error_code, - category, - error_message, + error_str, is_fatal, ); } @@ -9711,13 +9669,12 @@ class RealmLibrary { ffi.NativeFunction< ffi.Void Function( ffi.Pointer, - ffi.Int, - ffi.Int, + ffi.Int32, ffi.Pointer, ffi.Bool)>>('realm_sync_session_handle_error_for_testing'); late final _realm_sync_session_handle_error_for_testing = _realm_sync_session_handle_error_for_testingPtr.asFunction< - void Function(ffi.Pointer, int, int, + void Function(ffi.Pointer, int, ffi.Pointer, bool)>(); /// Ask the session to pause synchronization. @@ -11028,7 +10985,7 @@ class _SymbolAddresses { ffi.Pointer< ffi.NativeFunction< ffi.Void Function( - ffi.Pointer, ffi.Pointer)>> + ffi.Pointer, ffi.Pointer)>> get realm_dart_sync_wait_for_completion_callback => _library._realm_dart_sync_wait_for_completion_callbackPtr; ffi.Pointer)>> @@ -11329,6 +11286,22 @@ abstract class realm_errno { static const int RLM_ERR_SCHEMA_VERSION_MISMATCH = 1025; static const int RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE = 1026; static const int RLM_ERR_OPERATION_ABORTED = 1027; + static const int RLM_ERR_AUTO_CLIENT_RESET_FAILED = 1028; + static const int RLM_ERR_BAD_SYNC_PARTITION_VALUE = 1029; + static const int RLM_ERR_CONNECTION_CLOSED = 1030; + static const int RLM_ERR_INVALID_SUBSCRIPTION_QUERY = 1031; + static const int RLM_ERR_SYNC_CLIENT_RESET_REQUIRED = 1032; + static const int RLM_ERR_SYNC_COMPENSATING_WRITE = 1033; + static const int RLM_ERR_SYNC_CONNECT_FAILED = 1034; + static const int RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE = 1035; + static const int RLM_ERR_SYNC_PERMISSION_DENIED = 1036; + static const int RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED = 1037; + static const int RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED = 1038; + static const int RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED = 1039; + static const int RLM_ERR_SYNC_USER_MISMATCH = 1040; + static const int RLM_ERR_TLS_HANDSHAKE_FAILED = 1041; + static const int RLM_ERR_WRONG_SYNC_TYPE = 1042; + static const int RLM_ERR_SYNC_WRITE_NOT_ALLOWED = 1043; static const int RLM_ERR_SYSTEM_ERROR = 1999; static const int RLM_ERR_LOGIC = 2000; static const int RLM_ERR_NOT_SUPPORTED = 2001; @@ -11400,7 +11373,7 @@ abstract class realm_errno { static const int RLM_ERR_MONGODB_ERROR = 4311; static const int RLM_ERR_ARGUMENTS_NOT_ALLOWED = 4312; static const int RLM_ERR_FUNCTION_EXECUTION_ERROR = 4313; - static const int RLM_ERR_NO_MATCHING_RULE = 4314; + static const int RLM_ERR_NO_MATCHING_RULE_FOUND = 4314; static const int RLM_ERR_INTERNAL_SERVER_ERROR = 4315; static const int RLM_ERR_AUTH_PROVIDER_NOT_FOUND = 4316; static const int RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS = 4317; @@ -11442,9 +11415,6 @@ abstract class realm_errno { static const int RLM_ERR_USERPASS_TOKEN_INVALID = 4353; static const int RLM_ERR_INVALID_SERVER_RESPONSE = 4354; static const int RLM_ERR_APP_SERVER_ERROR = 4355; - static const int RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR = 4400; - static const int RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR = 4401; - static const int RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR = 4402; /// < A user-provided callback failed. static const int RLM_ERR_CALLBACK = 1000000; @@ -11944,9 +11914,7 @@ typedef realm_sync_connection_state_changed_func_t = ffi.Pointer< ffi.Int32 new_state)>>; final class realm_sync_error extends ffi.Struct { - external realm_sync_error_code_t error_code; - - external ffi.Pointer detailed_message; + external realm_error_t status; external ffi.Pointer c_original_file_path_key; @@ -11989,34 +11957,6 @@ abstract class realm_sync_error_action { static const int RLM_SYNC_ERROR_ACTION_REVERT_TO_PBS = 9; } -/// Possible error categories realm_sync_error_code_t can fall in. -abstract class realm_sync_error_category { - static const int RLM_SYNC_ERROR_CATEGORY_CLIENT = 0; - static const int RLM_SYNC_ERROR_CATEGORY_CONNECTION = 1; - static const int RLM_SYNC_ERROR_CATEGORY_SESSION = 2; - static const int RLM_SYNC_ERROR_CATEGORY_WEBSOCKET = 3; - - /// System error - POSIX errno, Win32 HRESULT, etc. - static const int RLM_SYNC_ERROR_CATEGORY_SYSTEM = 4; - - /// Unknown source of error. - static const int RLM_SYNC_ERROR_CATEGORY_UNKNOWN = 5; -} - -final class realm_sync_error_code extends ffi.Struct { - @ffi.Int32() - external int category; - - @ffi.Int() - external int value; - - external ffi.Pointer message; - - external ffi.Pointer category_name; -} - -typedef realm_sync_error_code_t = realm_sync_error_code; - final class realm_sync_error_compensating_write_info extends ffi.Struct { external ffi.Pointer reason; @@ -12149,8 +12089,8 @@ typedef realm_sync_ssl_verify_func_t = ffi.Pointer< /// @param error Null, if the operation completed successfully. typedef realm_sync_wait_for_completion_func_t = ffi.Pointer< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer userdata, - ffi.Pointer error)>>; + ffi.Void Function( + ffi.Pointer userdata, ffi.Pointer error)>>; typedef realm_t = shared_realm; final class realm_thread_safe_reference extends ffi.Opaque {} diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 48a171dee..a216ab6e4 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -631,10 +631,10 @@ class _RealmCore { }, unlockCallbackFunc); } - void raiseError(Session session, SyncErrorCategory category, int errorCode, bool isFatal) { + void raiseError(Session session, int errorCode, bool isFatal) { using((arena) { final message = "Simulated session error".toCharPtr(arena); - _realmLib.realm_sync_session_handle_error_for_testing(session.handle._pointer, errorCode, category.index, message, isFatal); + _realmLib.realm_sync_session_handle_error_for_testing(session.handle._pointer, errorCode, message, isFatal); }); } @@ -685,8 +685,9 @@ class _RealmCore { if (error != nullptr) { final err = arena(); - _realmLib.realm_get_async_error(error, err); - completer.completeError(RealmException("Failed to open realm ${err.ref.toLastError().toString()}")); + bool availableError = _realmLib.realm_get_async_error(error, err); + + completer.completeError(RealmException("Failed to open realm ${availableError ? err.ref.toLastError().toString() : ''}")); return; } @@ -1609,18 +1610,6 @@ class _RealmCore { _realmLib.realm_app_config_set_bundle_id(handle._pointer, getBundleId().toCharPtr(arena)); - if (configuration.localAppName != null) { - _realmLib.realm_app_config_set_local_app_name(handle._pointer, configuration.localAppName!.toCharPtr(arena)); - } else { - _realmLib.realm_app_config_set_local_app_name(handle._pointer, ''.toCharPtr(arena)); - } - - if (configuration.localAppVersion != null) { - _realmLib.realm_app_config_set_local_app_version(handle._pointer, configuration.localAppVersion!.toCharPtr(arena)); - } else { - _realmLib.realm_app_config_set_local_app_version(handle._pointer, ''.toCharPtr(arena)); - } - return handle; }); } @@ -2310,7 +2299,7 @@ class _RealmCore { Future sessionWaitForUpload(Session session) { final completer = Completer(); - final callback = Pointer.fromFunction)>(_sessionWaitCompletionCallback); + final callback = Pointer.fromFunction)>(_sessionWaitCompletionCallback); final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); _realmLib.realm_sync_session_wait_for_upload_completion(session.handle._pointer, _realmLib.addresses.realm_dart_sync_wait_for_completion_callback, userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); @@ -2320,7 +2309,7 @@ class _RealmCore { Future sessionWaitForDownload(Session session, [CancellationToken? cancellationToken]) { final completer = CancellableCompleter(cancellationToken); if (!completer.isCancelled) { - final callback = Pointer.fromFunction)>(_sessionWaitCompletionCallback); + final callback = Pointer.fromFunction)>(_sessionWaitCompletionCallback); final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); _realmLib.realm_sync_session_wait_for_download_completion(session.handle._pointer, _realmLib.addresses.realm_dart_sync_wait_for_completion_callback, userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); @@ -2328,7 +2317,7 @@ class _RealmCore { return completer.future; } - static void _sessionWaitCompletionCallback(Object userdata, Pointer errorCode) { + static void _sessionWaitCompletionCallback(Object userdata, Pointer errorCode) { final completer = userdata as Completer; if (completer.isCompleted) { return; @@ -3166,19 +3155,15 @@ extension on Pointer { extension on realm_sync_error { SyncErrorDetails toSyncErrorDetails() { - final message = error_code.message.cast().toRealmDartString()!; - final SyncErrorCategory category = SyncErrorCategory.values[error_code.category]; - final detailedMessage = detailed_message.cast().toRealmDartString(); - + final message = status.message.cast().toRealmDartString()!; final userInfoMap = user_info_map.toMap(user_info_length); final originalFilePathKey = c_original_file_path_key.cast().toRealmDartString(); final recoveryFilePathKey = c_recovery_file_path_key.cast().toRealmDartString(); return SyncErrorDetails( message, - category, - error_code.value, - detailedMessage: detailedMessage, + status.categories, + status.error, isFatal: is_fatal, isClientResetRequested: is_client_reset_requested, originalFilePath: userInfoMap?[originalFilePathKey], @@ -3221,10 +3206,10 @@ extension on Pointer { } } -extension on Pointer { +extension on Pointer { SyncError toSyncError() { final message = ref.message.cast().toDartString(); - final details = SyncErrorDetails(message, SyncErrorCategory.values[ref.category], ref.value); + final details = SyncErrorDetails(message, ref.categories, ref.error); return SyncErrorInternal.createSyncError(details); } } @@ -3382,25 +3367,30 @@ extension PlatformEx on Platform { /// @nodoc class SyncErrorDetails { final String message; - final SyncErrorCategory category; + final int _categoryFlag; final int code; - final String? detailedMessage; + final String? path; final bool isFatal; final bool isClientResetRequested; final String? originalFilePath; final String? backupFilePath; final List? compensatingWrites; - const SyncErrorDetails( + SyncErrorDetails( this.message, - this.category, + this._categoryFlag, this.code, { - this.detailedMessage, + this.path, this.isFatal = false, this.isClientResetRequested = false, this.originalFilePath, this.backupFilePath, this.compensatingWrites, }); + + // @categoryConstant param should be a value from `realm_error_category` class + bool _isFromCategory(int categoryConstant) { + return (_categoryFlag & categoryConstant) == categoryConstant; + } } extension on realm_error { @@ -3415,3 +3405,28 @@ extension on realm_error { return LastError(error, message, userError); } } + +/// @nodoc +class SyncErrorCodesConstants { + static const runtimeError = realm_errno.RLM_ERR_RUNTIME; //Sync + static const objectAlreadyExists = realm_errno.RLM_ERR_OBJECT_ALREADY_EXISTS; //Sync + static const badChangeset = realm_errno.RLM_ERR_BAD_CHANGESET; //Sync + static const autoClientResetFailed = realm_errno.RLM_ERR_AUTO_CLIENT_RESET_FAILED; //Sync + static const badSyncPartitionValue = realm_errno.RLM_ERR_BAD_SYNC_PARTITION_VALUE; //Sync + static const connectionClosed = realm_errno.RLM_ERR_CONNECTION_CLOSED; //Sync + static const invalidSubscriptionQuery = realm_errno.RLM_ERR_INVALID_SUBSCRIPTION_QUERY; //Sync + static const syncClientResetRequired = realm_errno.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED; //Sync + static const syncCompensatingWrite = realm_errno.RLM_ERR_SYNC_COMPENSATING_WRITE; //Sync + static const syncConnectFailed = realm_errno.RLM_ERR_SYNC_CONNECT_FAILED; //Sync + static const syncInvalidSchemaChange = realm_errno.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE; //Sync + static const syncPermissionDenied = realm_errno.RLM_ERR_SYNC_PERMISSION_DENIED; //Sync + static const syncProtocolInvariantFailed = realm_errno.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED; //Sync + static const syncProtocolNegotiationFailed = realm_errno.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED; //Sync + static const syncServerPermissionsChanged = realm_errno.RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED; //Sync + static const syncUserMismatch = realm_errno.RLM_ERR_SYNC_USER_MISMATCH; //Sync + static const tlsHandshakeFailed = realm_errno.RLM_ERR_TLS_HANDSHAKE_FAILED; //Sync + static const wrongSyncType = realm_errno.RLM_ERR_WRONG_SYNC_TYPE; //Sync + static const syncWriteNotAllowed = realm_errno.RLM_ERR_SYNC_WRITE_NOT_ALLOWED; //Sync + static const authError = realm_errno.RLM_ERR_AUTH_ERROR; //Sync + static const unknownError = realm_errno.RLM_ERR_UNKNOWN; //Sync +} diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 675e2007e..2b31803b2 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -87,7 +87,6 @@ export "configuration.dart" SyncConnectionError, SyncError, SyncErrorHandler, - SyncResolveError, SyncWebSocketError, SyncSessionError; export 'credentials.dart' show AuthProviderType, Credentials, EmailPasswordAuthProvider; @@ -119,9 +118,9 @@ export 'session.dart' SyncClientErrorCode, SyncConnectionErrorCode, SyncErrorCategory, - SyncResolveErrorCode, SyncWebSocketErrorCode, - SyncSessionErrorCode; + SyncSessionErrorCode, + SyncErrorCode; export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet; export 'user.dart' show User, UserState, ApiKeyClient, UserIdentity, ApiKey, FunctionsClient; export 'native/realm_core.dart' show Decimal128; diff --git a/lib/src/session.dart b/lib/src/session.dart index 26ca4b8f6..9d0f62d74 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -75,8 +75,8 @@ class Session implements Finalizable { return controller.createStream(); } - void _raiseSessionError(SyncErrorCategory category, int errorCode, bool isFatal) { - realmCore.raiseError(this, category, errorCode, isFatal); + void _raiseSessionError(SyncErrorCode errorCode, bool isFatal) { + realmCore.raiseError(this, errorCode.code, isFatal); } } @@ -121,8 +121,8 @@ extension SessionInternal on Session { return _handle; } - void raiseError(SyncErrorCategory category, int errorCode, bool isFatal) { - realmCore.raiseError(this, category, errorCode, isFatal); + void raiseError(SyncErrorCode errorCode, bool isFatal) { + realmCore.raiseError(this, errorCode.code, isFatal); } static SyncProgress createSyncProgress(int transferredBytes, int transferableBytes) => @@ -246,6 +246,7 @@ enum ProgressMode { } /// The category of a [SyncError]. +@Deprecated("There are no more error categories for sync errors") enum SyncErrorCategory { /// The error originated from the client client, @@ -271,6 +272,7 @@ enum SyncErrorCategory { /// These errors will terminate the network connection /// (disconnect all sessions associated with the affected connection), /// and the error will be reported via the connection state change listeners of the affected sessions. +@Deprecated("Use SyncErrorCode enum instead") enum SyncClientErrorCode { /// Connection closed (no error) connectionClosed(100), @@ -382,6 +384,7 @@ enum SyncClientErrorCode { /// Protocol connection errors discovered by the server, and reported to the client /// /// These errors will be reported via the error handlers of the affected sessions. +@Deprecated("Use SyncErrorCode enum instead") enum SyncConnectionErrorCode { // Connection level and protocol errors /// Connection closed (no error) @@ -445,6 +448,7 @@ enum SyncConnectionErrorCode { /// Protocol session errors discovered by the server, and reported to the client /// /// These errors will be reported via the error handlers of the affected sessions. +@Deprecated("Use SyncErrorCode enum instead") enum SyncSessionErrorCode { /// Session closed (no error) sessionClosed(200), @@ -557,47 +561,10 @@ enum SyncSessionErrorCode { const SyncSessionErrorCode(this.code); } -/// Protocol network resolution errors. -/// -/// These errors will be reported via the error handlers of the affected sessions. -/// This enum is deprecated and it will be removed. -/// Use [SyncWebSocketErrorCode] instead. -@Deprecated("Use SyncWebSocketErrorCode instead") -enum SyncResolveErrorCode { - /// Host not found (authoritative). - hostNotFound(1), - - /// Host not found (non-authoritative). - hostNotFoundTryAgain(2), - - /// The query is valid but does not have associated address data. - noData(3), - - /// A non-recoverable error occurred. - noRecovery(4), - - /// The service is not supported for the given socket type. - serviceNotFound(5), - - /// The socket type is not supported. - socketTypeNotSupported(6), - - /// Unknown resolve errors - unknown(1000); - - static final Map _valuesMap = {for (var value in SyncResolveErrorCode.values) value.code: value}; - - static SyncResolveErrorCode fromInt(int code) { - return SyncResolveErrorCode._valuesMap[code] ?? SyncResolveErrorCode.unknown; - } - - final int code; - const SyncResolveErrorCode(this.code); -} - /// Web socket errors. /// /// These errors will be reported via the error handlers of the affected sessions. +@Deprecated("Use SyncErrorCode instead") enum SyncWebSocketErrorCode { /// Web socket resolution failed websocketResolveFailed(4400), @@ -620,3 +587,74 @@ enum SyncWebSocketErrorCode { final int code; const SyncWebSocketErrorCode(this.code); } + +/// Sync errors caused by the client, server or the connection. +/// +/// These errors will be reported via the error handlers of the affected sessions. +enum SyncErrorCode { + /// Connection closed by the server + connectionClosed(SyncErrorCodesConstants.connectionClosed), + + invariantFailed(SyncErrorCodesConstants.syncProtocolInvariantFailed), + negotiationFailed(SyncErrorCodesConstants.syncProtocolNegotiationFailed), + + /// Bad changeset (UPLOAD) + badChangeset(SyncErrorCodesConstants.badChangeset), + + /// SSL server certificate rejected + sslServerCertRejected(SyncErrorCodesConstants.tlsHandshakeFailed), + + /// Sync connection was not fully established in time + connectTimeout(SyncErrorCodesConstants.syncConnectFailed), + + /// A fatal error was encountered which prevents completion of a client reset + autoClientResetFailure(SyncErrorCodesConstants.autoClientResetFailed), + + /// Connected with wrong wire protocol - should switch to FLX sync + switchToFlxSync(SyncErrorCodesConstants.wrongSyncType), + + runtimeError(SyncErrorCodesConstants.runtimeError), + + /// Illegal Realm path (BIND) + illegalRealmPath(SyncErrorCodesConstants.badSyncPartitionValue), + + /// Illegal Realm path (BIND) + permissionDenied(SyncErrorCodesConstants.syncPermissionDenied), + syncClientResetRequired(SyncErrorCodesConstants.syncClientResetRequired), + + /// User mismatch for client file identifier (IDENT) + userMismatch(SyncErrorCodesConstants.syncUserMismatch), + + /// Invalid schema change (UPLOAD) + invalidSchemaChange(SyncErrorCodesConstants.syncInvalidSchemaChange), + + /// Client query is invalid/malformed (IDENT, QUERY) + badQuery(SyncErrorCodesConstants.invalidSubscriptionQuery), + + /// Client tried to create an object that already exists outside their (()UPLOAD) + objectAlreadyExists(SyncErrorCodesConstants.objectAlreadyExists), + + /// Server permissions for this file ident have changed since the last time it (used) (IDENT) + serverPermissionsChanged(SyncErrorCodesConstants.syncServerPermissionsChanged), + + /// Client attempted a write that is disallowed by permissions, or modifies an object + /// outside the current query - requires client reset (UPLOAD) + writeNotAllowed(SyncErrorCodesConstants.syncWriteNotAllowed), + + /// Client attempted a write that is disallowed by permissions, or modifies an object + /// outside the current query, and the server undid the modification (UPLOAD) + compensatingWrite(SyncErrorCodesConstants.syncCompensatingWrite), + + /// Unknown Sync client error code + unknown(9999); + + static final Map _valuesMap = {for (var value in SyncErrorCode.values) value.code: value}; + + static SyncErrorCode fromInt(int code) { + return SyncErrorCode._valuesMap[code] ?? SyncErrorCode.unknown; + } + + final int code; + + const SyncErrorCode(this.code); +} diff --git a/src/realm-core b/src/realm-core index 21925e4d8..f6193f93b 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit 21925e4d81598959bd128ad0e687ce95efd12c45 +Subproject commit f6193f93b71726a47d00c77022fcd96ab2589bb4 diff --git a/src/realm_dart_sync.cpp b/src/realm_dart_sync.cpp index 8846e3aae..c76e7f36c 100644 --- a/src/realm_dart_sync.cpp +++ b/src/realm_dart_sync.cpp @@ -69,7 +69,8 @@ RLM_API void realm_dart_sync_error_handler_callback(realm_userdata_t userdata, r struct error_copy { std::string message; - std::string detailed_message; + realm_errno_e error; + realm_error_categories categories; std::string original_file_path_key; std::string recovery_file_path_key; bool is_fatal; @@ -80,8 +81,9 @@ RLM_API void realm_dart_sync_error_handler_callback(realm_userdata_t userdata, r std::vector compensating_writes_errors_info; } buf; - buf.message = error.error_code.message; - buf.detailed_message = std::string(error.detailed_message); + buf.message = std::string(error.status.message); + buf.categories = error.status.categories; + buf.error = error.status.error; buf.original_file_path_key = std::string(error.c_original_file_path_key); buf.recovery_file_path_key = std::string(error.c_recovery_file_path_key); buf.is_fatal = error.is_fatal; @@ -112,8 +114,9 @@ RLM_API void realm_dart_sync_error_handler_callback(realm_userdata_t userdata, r auto ud = reinterpret_cast(userdata); ud->scheduler->invoke([ud, session = *session, error = std::move(error), buf = std::move(buf)]() mutable { //we moved buf so we need to update the error pointers here. - error.error_code.message = buf.message.c_str(); - error.detailed_message = buf.detailed_message.c_str(); + error.status.message = buf.message.c_str(); + error.status.error = buf.error; + error.status.categories = buf.categories; error.c_original_file_path_key = buf.original_file_path_key.c_str(); error.c_recovery_file_path_key = buf.recovery_file_path_key.c_str(); error.is_fatal = buf.is_fatal; @@ -124,16 +127,19 @@ RLM_API void realm_dart_sync_error_handler_callback(realm_userdata_t userdata, r }); } -RLM_API void realm_dart_sync_wait_for_completion_callback(realm_userdata_t userdata, realm_sync_error_code_t* error) +RLM_API void realm_dart_sync_wait_for_completion_callback(realm_userdata_t userdata, realm_error_t* error) { // we need to make a deep copy of error, because the message pointer points to stack memory - struct realm_dart_sync_error_code : realm_sync_error_code + struct realm_dart_sync_error_code : realm_error_t { - realm_dart_sync_error_code(const realm_sync_error_code& error) - : realm_sync_error_code(error) - , message_buffer(error.message) + realm_dart_sync_error_code(const realm_error& error_input) + : message_buffer(error_input.message) { - message = message_buffer.c_str(); + error = error_input.error; + categories = error_input.categories; + usercode_error = error_input.usercode_error; + path = error_input.path; + message = message_buffer.c_str(); } const std::string message_buffer; diff --git a/src/realm_dart_sync.h b/src/realm_dart_sync.h index 8d1f9f412..31ab551be 100644 --- a/src/realm_dart_sync.h +++ b/src/realm_dart_sync.h @@ -29,7 +29,7 @@ RLM_API void realm_dart_sync_client_log_callback(realm_userdata_t userdata, real RLM_API void realm_dart_sync_error_handler_callback(realm_userdata_t userdata, realm_sync_session_t* session, realm_sync_error_t error); -RLM_API void realm_dart_sync_wait_for_completion_callback(realm_userdata_t userdata, realm_sync_error_code_t* error); +RLM_API void realm_dart_sync_wait_for_completion_callback(realm_userdata_t userdata, realm_error_t* error); RLM_API void realm_dart_sync_progress_callback(realm_userdata_t userdata, uint64_t transferred_bytes, uint64_t total_bytes); diff --git a/test/app_test.dart b/test/app_test.dart index 9c6445d5b..5b0d9f74f 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -44,8 +44,6 @@ Future main([List? args]) async { baseFilePath: Directory.systemTemp, baseUrl: Uri.parse('https://not_re.al'), defaultRequestTimeout: const Duration(seconds: 2), - localAppName: 'bar', - localAppVersion: "1.0.0", metadataPersistenceMode: MetadataPersistenceMode.disabled, maxConnectionTimeout: const Duration(minutes: 1), httpClient: httpClient, @@ -79,8 +77,6 @@ Future main([List? args]) async { baseFilePath: Directory.systemTemp, baseUrl: Uri.parse('https://not_re.al'), defaultRequestTimeout: const Duration(seconds: 2), - localAppName: 'bar', - localAppVersion: "1.0.0", metadataPersistenceMode: MetadataPersistenceMode.encrypted, metadataEncryptionKey: base64.decode("ekey"), maxConnectionTimeout: const Duration(minutes: 1), diff --git a/test/client_reset_test.dart b/test/client_reset_test.dart index 211ea1878..4400866ff 100644 --- a/test/client_reset_test.dart +++ b/test/client_reset_test.dart @@ -457,9 +457,7 @@ Future main([List? args]) async { expect(onAfterResetOccurred, 1); expect(onBeforeResetOccurred, 1); - expect(clientResetErrorOnManualFallback.category, SyncErrorCategory.client); - expect(clientResetErrorOnManualFallback.code, SyncClientErrorCode.autoClientResetFailure); - expect(clientResetErrorOnManualFallback.sessionErrorCode, SyncSessionErrorCode.unknown); + expect(clientResetErrorOnManualFallback.errorCode, SyncErrorCode.autoClientResetFailure); }); // 1. userA adds [task0, task1, task2] and syncs it, then disconnects @@ -554,13 +552,9 @@ Future main([List? args]) async { await triggerClientReset(realm); await resetCompleter.future.wait(defaultWaitTimeout, "ClientResetError is not reported."); - expect(clientResetError.category, SyncErrorCategory.session); - expect(clientResetError.code, SyncClientErrorCode.unknown); - expect(clientResetError.sessionErrorCode, SyncSessionErrorCode.badClientFileIdent); + expect(clientResetError.errorCode, SyncErrorCode.autoClientResetFailure); expect(clientResetError.isFatal, isTrue); expect(clientResetError.message, isNotEmpty); - expect(clientResetError.detailedMessage, isNotEmpty); - expect(clientResetError.message == clientResetError.detailedMessage, isFalse); expect(clientResetError.backupFilePath, isNotEmpty); }); } diff --git a/test/session_test.dart b/test/session_test.dart index 27e1a4d36..ceaae5e45 100644 --- a/test/session_test.dart +++ b/test/session_test.dart @@ -283,35 +283,27 @@ Future main([List? args]) async { final app = App(configuration); final user = await getIntegrationUser(app); final config = Configuration.flexibleSync(user, [Task.schema], syncErrorHandler: (syncError) { - expect(syncError, isA()); - final sessionError = syncError.as(); - expect(sessionError.category, SyncErrorCategory.session); - expect(sessionError.isFatal, false); - expect(sessionError.code, SyncSessionErrorCode.badAuthentication); - expect(sessionError.detailedMessage, "Simulated session error"); - expect(sessionError.message, "Bad user authentication (BIND)"); + expect(syncError.isFatal, isFalse); + expect(syncError.errorCode, SyncErrorCode.badQuery); + expect(syncError.message, "Bad user authentication (BIND)"); }); final realm = getRealm(config); - realm.syncSession.raiseError(SyncErrorCategory.session, SyncSessionErrorCode.badAuthentication.code, false); + realm.syncSession.raiseError(SyncErrorCode.badQuery, false); }); baasTest('SyncSession test fatal error handler', (configuration) async { final app = App(configuration); final user = await getIntegrationUser(app); final config = Configuration.flexibleSync(user, [Task.schema], syncErrorHandler: (syncError) { - expect(syncError, isA()); - final syncClientError = syncError.as(); - expect(syncClientError.category, SyncErrorCategory.client); - expect(syncClientError.isFatal, true); - expect(syncClientError.code, SyncClientErrorCode.badChangeset); - expect(syncClientError.detailedMessage, "Simulated session error"); - expect(syncClientError.message, "Bad changeset (DOWNLOAD)"); + expect(syncError.isFatal, isTrue); + expect(syncError, SyncErrorCode.badChangeset); + expect(syncError.message, "Bad changeset (DOWNLOAD)"); }); final realm = getRealm(config); - realm.syncSession.raiseError(SyncErrorCategory.client, SyncClientErrorCode.badChangeset.code, true); + realm.syncSession.raiseError(SyncErrorCode.badChangeset, true); }); baasTest('SyncSession.getConnectionStateStream', (configuration) async { @@ -366,27 +358,26 @@ Future main([List? args]) async { expect(() => session.state, throws()); }); - for (SyncWebSocketErrorCode errorCode in SyncWebSocketErrorCode.values.where((v) => v != SyncWebSocketErrorCode.unknown)) { - baasTest('Sync Web Socket Error ${errorCode.name}', (configuration) async { - final app = App(configuration); - final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync( - user, - [Task.schema], - syncErrorHandler: (syncError) { - expect(syncError, isA()); - final sessionError = syncError.as(); - expect(sessionError.category, SyncErrorCategory.webSocket); - expect(sessionError.code, errorCode); - expect(sessionError.detailedMessage, "Simulated session error"); - expect(sessionError.detailedMessage, isNot(sessionError.message)); - expect(syncError.codeValue, errorCode.code); - }, - ); - final realm = getRealm(config); - realm.syncSession.raiseError(SyncErrorCategory.webSocket, errorCode.code, false); - }); - } +// for (SyncWebSocketErrorCode errorCode in SyncWebSocketErrorCode.values.where((v) => v != SyncWebSocketErrorCode.unknown)) { +// baasTest('Sync Web Socket Error ${errorCode.name}', (configuration) async { +// final app = App(configuration); +// final user = await getIntegrationUser(app); +// final config = Configuration.flexibleSync( +// user, +// [Task.schema], +// syncErrorHandler: (syncError) { +// expect(syncError, isA()); +// final sessionError = syncError.as(); +// expect(sessionError.category, SyncErrorCategory.webSocket); +// expect(sessionError.code, errorCode); +// expect(sessionError.message, "Simulated session error"); +// expect(syncError.codeValue, errorCode.code); +// }, +// ); +// final realm = getRealm(config); +// realm.syncSession.raiseError(SyncErrorCategory.webSocket, errorCode.code, false); +// }); +// } } class StreamProgressData { diff --git a/test/subscription_test.dart b/test/subscription_test.dart index a059d12fe..95aa7ef01 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -582,12 +582,9 @@ Future main([List? args]) async { expect(compensatingWriteError, isA()); final sessionError = compensatingWriteError.as(); - expect(sessionError.category, SyncErrorCategory.session); - expect(sessionError.code, SyncSessionErrorCode.compensatingWrite); - expect(sessionError.message!.startsWith('Client attempted a write that is disallowed by permissions, or modifies an object outside the current query'), + expect(sessionError.errorCode, SyncErrorCode.compensatingWrite); + expect(sessionError.message!.startsWith('Client attempted a write that is outside of permissions or query filters'), isTrue); - expect(sessionError.detailedMessage, isNotEmpty); - expect(sessionError.message == sessionError.detailedMessage, isFalse); expect(sessionError.compensatingWrites, isNotNull); final writeReason = sessionError.compensatingWrites!.first; expect(writeReason, isNotNull);