diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c229f3a..b4600273a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Breaking Changes * Fixed an issue that would cause passwords sent to the server (e.g. `Credentials.EmailPassword` or `EmailPasswordAuthProvider.registerUser`) to contain an extra empty byte at the end. (PR [#918](https://github.com/realm/realm-dart/pull/918)). - Notice: Any existing email users might need to be recreated because of this breaking change. + Notice: Any existing email users might need to be recreated because of this breaking change. ### Enhancements * Added support for "frozen objects" - these are objects, queries, lists, or Realms that have been "frozen" at a specific version. All frozen objects can be accessed and queried as normal, but attempting to mutate them or add change listeners will throw an exception. `Realm`, `RealmObject`, `RealmList`, and `RealmResults` now have a method `freeze()` which returns an immutable version of the object, as well as an `isFrozen` property which can be used to check whether an object is frozen. ([#56](https://github.com/realm/realm-dart/issues/56)) @@ -52,7 +52,7 @@ * Previously removeAt did not truncate length. ([#883](https://github.com/realm/realm-dart/issues/883)) * List.length= now throws, if you try to increase length. This previously succeeded silently. ([#894](https://github.com/realm/realm-dart/pull/894)). * Queries on lists were broken. ([#909](https://github.com/realm/realm-dart/issues/909)) - Example: + Example: ```dart expect(realm.all(), [alice, bob, carol, dan]); // assume this pass, then ... expect(team.players.query('TRUEPREDICATE'), [alice, bob]); // <-- ... this fails and return the same as realm.all() @@ -71,6 +71,7 @@ ### Internal * Uses Realm Core v12.9.0 +* Added tracking of child handles for objects/results/lists obtained from an unowned Realm. This ensures that all children are invalidated as soon as the parent Realm gets released at the end of the callback. (Issue [#527](https://github.com/realm/realm-dart/issues/527)) ## 0.4.0+beta (2022-08-19) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index c42afbaff..c42d6f0e9 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -68,6 +68,12 @@ class RealmError extends Error { String toString() => "Realm error : $message"; } +/// An error throw when operating on an object that has been closed. +/// {@category Realm} +class RealmClosedError extends RealmError { + RealmClosedError(String message) : super(message); +} + /// Thrown if the operation is not supported. /// {@category Realm} class RealmUnsupportedSetError extends UnsupportedError implements RealmError { @@ -78,6 +84,7 @@ class RealmUnsupportedSetError extends UnsupportedError implements RealmError { class RealmStateError extends StateError implements RealmError { RealmStateError(super.message); } + /// @nodoc class Decimal128 {} // TODO Support decimal128 datatype https://github.com/realm/realm-dart/issues/725 diff --git a/lib/src/list.dart b/lib/src/list.dart index df2320010..711b9367e 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -60,7 +60,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } @override - int get length => realmCore.getListSize(_handle); + int get length => realmCore.getListSize(handle); /// Setting the `length` is a required method on [List], but makes less sense /// for [RealmList]s. You can only decrease the length, increasing it doesn't @@ -223,7 +223,14 @@ extension RealmListInternal on RealmList { ManagedRealmList asManaged() => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); - RealmListHandle get handle => asManaged()._handle; + RealmListHandle get handle { + final result = asManaged()._handle; + if (result.released) { + throw RealmClosedError('Cannot access a list that belongs to a closed Realm'); + } + + return result; + } RealmObjectMetadata? get metadata => asManaged()._metadata; diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index e60c73d6f..1a829697d 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -271,7 +271,7 @@ class _RealmCore { } SubscriptionSetHandle getSubscriptions(Realm realm) { - return SubscriptionSetHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_get_active_subscription_set(realm.handle._pointer))); + return SubscriptionSetHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_get_active_subscription_set(realm.handle._pointer)), realm.handle); } void refreshSubscriptions(SubscriptionSet subscriptions) { @@ -337,11 +337,13 @@ class _RealmCore { } MutableSubscriptionSetHandle subscriptionSetMakeMutable(SubscriptionSet subscriptions) { - return MutableSubscriptionSetHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_make_subscription_set_mutable(subscriptions.handle._pointer))); + return MutableSubscriptionSetHandle._( + _realmLib.invokeGetPointer(() => _realmLib.realm_sync_make_subscription_set_mutable(subscriptions.handle._pointer)), subscriptions.realm.handle); } SubscriptionSetHandle subscriptionSetCommit(MutableSubscriptionSet subscriptions) { - return SubscriptionSetHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_subscription_set_commit(subscriptions.handle._mutablePointer))); + return SubscriptionSetHandle._( + _realmLib.invokeGetPointer(() => _realmLib.realm_sync_subscription_set_commit(subscriptions.handle._mutablePointer)), subscriptions.realm.handle); } SubscriptionHandle insertOrAssignSubscription(MutableSubscriptionSet subscriptions, RealmResults results, String? name, bool update) { @@ -408,18 +410,21 @@ class _RealmCore { _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_refresh(subscriptions.handle._pointer)); } - static bool initial_data_callback(Pointer userdata, Pointer realmHandle) { + static bool initial_data_callback(Pointer userdata, Pointer realmPtr) { + final realmHandle = RealmHandle._unowned(realmPtr); try { final LocalConfiguration? config = userdata.toObject(); if (config == null) { return false; } - final realm = RealmInternal.getUnowned(config, RealmHandle._unowned(realmHandle)); + final realm = RealmInternal.getUnowned(config, realmHandle); config.initialDataCallback!(realm); return true; } catch (ex) { // TODO: Propagate error to Core in initial_data_callback https://github.com/realm/realm-dart/issues/698 // Core issue: https://github.com/realm/realm-core/issues/5366 + } finally { + realmHandle.release(); } return false; @@ -436,6 +441,8 @@ class _RealmCore { static bool migration_callback( Pointer userdata, Pointer oldRealmHandle, Pointer newRealmHandle, Pointer schema) { + final oldHandle = RealmHandle._unowned(oldRealmHandle); + final newHandle = RealmHandle._unowned(newRealmHandle); try { final LocalConfiguration? config = userdata.toObject(); if (config == null) { @@ -444,16 +451,20 @@ class _RealmCore { final oldSchemaVersion = _realmLib.realm_get_schema_version(oldRealmHandle); final oldConfig = Configuration.local([], path: config.path, isReadOnly: true, schemaVersion: oldSchemaVersion); - final oldRealm = RealmInternal.getUnowned(oldConfig, RealmHandle._unowned(oldRealmHandle), isInMigration: true); + final oldRealm = RealmInternal.getUnowned(oldConfig, oldHandle, isInMigration: true); - final newRealm = RealmInternal.getUnowned(config, RealmHandle._unowned(newRealmHandle), isInMigration: true); + final newRealm = RealmInternal.getUnowned(config, newHandle, isInMigration: true); final migration = MigrationInternal.create(RealmInternal.getMigrationRealm(oldRealm), newRealm, SchemaHandle.unowned(schema)); config.migrationCallback!(migration, oldSchemaVersion); return true; } catch (ex) { _realmLib.realm_register_user_code_callback_error(ex.toPersistentHandle()); + } finally { + oldHandle.release(); + newHandle.release(); } + return false; } @@ -625,7 +636,7 @@ class _RealmCore { RealmObjectHandle createRealmObject(Realm realm, int classKey) { final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_object_create(realm.handle._pointer, classKey)); - return RealmObjectHandle._(realmPtr); + return RealmObjectHandle._(realmPtr, realm.handle); } RealmObjectHandle getOrCreateRealmObjectWithPrimaryKey(Realm realm, int classKey, Object? primaryKey) { @@ -638,7 +649,7 @@ class _RealmCore { realm_value.ref, didCreate, )); - return RealmObjectHandle._(realmPtr); + return RealmObjectHandle._(realmPtr, realm.handle); }); } @@ -646,7 +657,7 @@ class _RealmCore { return using((Arena arena) { final realm_value = _toRealmValue(primaryKey, arena); final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_object_create_with_primary_key(realm.handle._pointer, classKey, realm_value.ref)); - return RealmObjectHandle._(realmPtr); + return RealmObjectHandle._(realmPtr, realm.handle); }); } @@ -681,14 +692,14 @@ class _RealmCore { return null; } - return RealmObjectHandle._(pointer); + return RealmObjectHandle._(pointer, realm.handle); }); } RealmObjectHandle? findExisting(Realm realm, int classKey, RealmObjectHandle other) { final key = _realmLib.realm_object_get_key(other._pointer); final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_object(realm.handle._pointer, classKey, key)); - return RealmObjectHandle._(pointer); + return RealmObjectHandle._(pointer, realm.handle); } void renameProperty(Realm realm, String objectType, String oldName, String newName, SchemaHandle schema) { @@ -712,7 +723,7 @@ class _RealmCore { RealmResultsHandle findAll(Realm realm, int classKey) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_object_find_all(realm.handle._pointer, classKey)); - return RealmResultsHandle._(pointer); + return RealmResultsHandle._(pointer, realm.handle); } RealmResultsHandle queryClass(Realm realm, int classKey, String query, List args) { @@ -722,15 +733,17 @@ class _RealmCore { for (var i = 0; i < length; ++i) { _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); } - final queryHandle = RealmQueryHandle._(_realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse( - realm.handle._pointer, - classKey, - query.toCharPtr(arena), - length, - argsPointer, - ), - )); + final queryHandle = _RealmQueryHandle._( + _realmLib.invokeGetPointer( + () => _realmLib.realm_query_parse( + realm.handle._pointer, + classKey, + query.toCharPtr(arena), + length, + argsPointer, + ), + ), + realm.handle); return _queryFindAll(queryHandle); }); } @@ -742,21 +755,27 @@ class _RealmCore { for (var i = 0; i < length; ++i) { _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); } - final queryHandle = RealmQueryHandle._(_realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse_for_results( - target.handle._pointer, - query.toCharPtr(arena), - length, - argsPointer, - ), - )); + final queryHandle = _RealmQueryHandle._( + _realmLib.invokeGetPointer( + () => _realmLib.realm_query_parse_for_results( + target.handle._pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + ), + target.realm.handle); return _queryFindAll(queryHandle); }); } - RealmResultsHandle _queryFindAll(RealmQueryHandle queryHandle) { - final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_query_find_all(queryHandle._pointer)); - return RealmResultsHandle._(resultsPointer); + RealmResultsHandle _queryFindAll(_RealmQueryHandle queryHandle) { + try { + final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_query_find_all(queryHandle._pointer)); + return RealmResultsHandle._(resultsPointer, queryHandle._root); + } finally { + queryHandle.release(); + } } RealmResultsHandle queryList(RealmList target, String query, List args) { @@ -766,21 +785,23 @@ class _RealmCore { for (var i = 0; i < length; ++i) { _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); } - final queryHandle = RealmQueryHandle._(_realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse_for_list( - target.handle._pointer, - query.toCharPtr(arena), - length, - argsPointer, - ), - )); + final queryHandle = _RealmQueryHandle._( + _realmLib.invokeGetPointer( + () => _realmLib.realm_query_parse_for_list( + target.handle._pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + ), + target.realm.handle); return _queryFindAll(queryHandle); }); } RealmObjectHandle getObjectAt(RealmResults results, int index) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_get_object(results.handle._pointer, index)); - return RealmObjectHandle._(pointer); + return RealmObjectHandle._(pointer, results.realm.handle); } int getResultsCount(RealmResults results) { @@ -842,19 +863,19 @@ class _RealmCore { }); } - RealmLinkHandle _getObjectAsLink(RealmObject object) { + _RealmLinkHandle _getObjectAsLink(RealmObject object) { final realmLink = _realmLib.realm_object_as_link(object.handle._pointer); - return RealmLinkHandle._(realmLink); + return _RealmLinkHandle._(realmLink); } RealmObjectHandle _getObject(Realm realm, int classKey, int objectKey) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_object(realm.handle._pointer, classKey, objectKey)); - return RealmObjectHandle._(pointer); + return RealmObjectHandle._(pointer, realm.handle); } RealmListHandle getListProperty(RealmObject object, int propertyKey) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_list(object.handle._pointer, propertyKey)); - return RealmListHandle._(pointer); + return RealmListHandle._(pointer, object.realm.handle); } int getListSize(RealmListHandle handle) { @@ -933,7 +954,7 @@ class _RealmCore { RealmResultsHandle resultsSnapshot(RealmResults results) { final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_snapshot(results.handle._pointer)); - return RealmResultsHandle._(resultsPointer); + return RealmResultsHandle._(resultsPointer, results.realm.handle); } bool objectIsValid(RealmObject object) { @@ -1003,7 +1024,7 @@ class _RealmCore { Pointer.fromFunction(collection_change_callback), )); - return RealmNotificationTokenHandle._(pointer); + return RealmNotificationTokenHandle._(pointer, results.realm.handle); } RealmNotificationTokenHandle subscribeListNotifications(RealmList list, NotificationsController controller) { @@ -1015,7 +1036,7 @@ class _RealmCore { Pointer.fromFunction(collection_change_callback), )); - return RealmNotificationTokenHandle._(pointer); + return RealmNotificationTokenHandle._(pointer, list.realm.handle); } RealmNotificationTokenHandle subscribeObjectNotifications(RealmObject object, NotificationsController controller) { @@ -1027,7 +1048,7 @@ class _RealmCore { Pointer.fromFunction(object_change_callback), )); - return RealmNotificationTokenHandle._(pointer); + return RealmNotificationTokenHandle._(pointer, object.realm.handle); } bool getObjectChangesIsDeleted(RealmObjectChangesHandle handle) { @@ -1670,7 +1691,7 @@ class _RealmCore { } SessionHandle realmGetSession(Realm realm) { - return SessionHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_session_get(realm.handle._pointer))); + return SessionHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_session_get(realm.handle._pointer)), realm.handle); } String sessionGetPath(Session session) { @@ -1853,7 +1874,7 @@ class _RealmCore { RealmResultsHandle resolveResults(RealmResults realmResults, Realm frozenRealm) { final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_results_resolve_in(realmResults.handle._pointer, frozenRealm.handle._pointer)); - return RealmResultsHandle._(ptr); + return RealmResultsHandle._(ptr, frozenRealm.handle); } RealmObjectHandle? resolveObject(RealmObject object, Realm frozenRealm) { @@ -1861,7 +1882,7 @@ class _RealmCore { final resultPtr = arena>(); _realmLib.invokeGetBool(() => _realmLib.realm_object_resolve_in(object.handle._pointer, frozenRealm.handle._pointer, resultPtr)); if (resultPtr != nullptr) { - return RealmObjectHandle._(resultPtr.value); + return RealmObjectHandle._(resultPtr.value, frozenRealm.handle); } return null; @@ -1873,7 +1894,7 @@ class _RealmCore { final resultPtr = arena>(); _realmLib.invokeGetBool(() => _realmLib.realm_list_resolve_in(list.handle._pointer, frozenRealm.handle._pointer, resultPtr)); if (resultPtr != nullptr) { - return RealmListHandle._(resultPtr.value); + return RealmListHandle._(resultPtr.value, frozenRealm.handle); } return null; @@ -2042,20 +2063,82 @@ void _tearDownFinalizationTrace(Object value, Object finalizationToken) { final _nativeFinalizer = NativeFinalizer(_realmLib.addresses.realm_release); abstract class HandleBase implements Finalizable { - final Pointer _pointer; + Pointer _pointer; + bool get released => _pointer == nullptr; + final bool isUnowned; @pragma('vm:never-inline') void keepAlive() {} - HandleBase(this._pointer, int size) { + HandleBase(this._pointer, int size) : isUnowned = false { _nativeFinalizer.attach(this, _pointer.cast(), detach: this, externalSize: size); - if (_enableFinalizerTrace) _setupFinalizationTrace(this, _pointer); + if (_enableFinalizerTrace) { + _setupFinalizationTrace(this, _pointer); + } } - HandleBase.unowned(this._pointer); + HandleBase.unowned(this._pointer) : isUnowned = true; + + @override + String toString() => "${_pointer.toString()} value=${_pointer.cast().value}${isUnowned ? ' (unowned)' : ''}"; + + /// @nodoc + /// A method that will be invoked just before the handle is released. Allows to cleanup + /// any custom data that inheritors are storing. + void _releaseCore() {} + + void release() { + if (released) { + return; + } + + _releaseCore(); + + if (!isUnowned) { + _nativeFinalizer.detach(this); + _realmLib.realm_release(_pointer.cast()); + } + + _pointer = nullptr; + + if (_enableFinalizerTrace) { + _tearDownFinalizationTrace(this, _pointer); + } + } +} + +class FinalizationToken { + final WeakReference root; + final int id; + + FinalizationToken(RealmHandle handle, this.id) : root = WeakReference(handle); +} + +// This finalizer is intended to prevent the list of children in the RealmHandle +// from growing endlessly. It's not intended to replace the native finalizer which +// will free the actual resources owned by the handle. +final _rootedHandleFinalizer = Finalizer((token) { + token.root.target?.removeChild(token.id); +}); + +abstract class RootedHandleBase extends HandleBase { + final RealmHandle _root; + int? _id; + + bool get shouldRoot => _root.isUnowned; + + RootedHandleBase(this._root, Pointer pointer, int size) : super(pointer, size) { + if (shouldRoot) { + _id = _root.addChild(this); + } + } @override - String toString() => "${_pointer.toString()} value=${_pointer.cast().value}"; + void _releaseCore() { + if (_id != null) { + _root.removeChild(_id!); + } + } } class SchemaHandle extends HandleBase { @@ -2069,59 +2152,71 @@ class ConfigHandle extends HandleBase { } class RealmHandle extends HandleBase { + int _counter = 0; + + final Map> _children = {}; + RealmHandle._(Pointer pointer) : super(pointer, 24); RealmHandle._unowned(Pointer pointer) : super.unowned(pointer); + + int addChild(RootedHandleBase child) { + final id = _counter++; + _children[id] = WeakReference(child); + _rootedHandleFinalizer.attach(this, FinalizationToken(this, id), detach: this); + return id; + } + + void removeChild(int id) { + final child = _children.remove(id); + if (child != null) { + final target = child.target; + if (target != null) { + _rootedHandleFinalizer.detach(target); + } + } + } + + @override + void _releaseCore() { + final keys = _children.keys.toList(); + + for (final key in keys) { + _children[key]?.target?.release(); + } + } } class SchedulerHandle extends HandleBase { SchedulerHandle._(Pointer pointer) : super(pointer, 24); } -class RealmObjectHandle extends HandleBase { - RealmObjectHandle._(Pointer pointer) : super(pointer, 112); +class RealmObjectHandle extends RootedHandleBase { + RealmObjectHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 112); } -class RealmLinkHandle { +class _RealmLinkHandle { final int targetKey; final int classKey; - RealmLinkHandle._(realm_link_t link) + _RealmLinkHandle._(realm_link_t link) : targetKey = link.target, classKey = link.target_table; } -class RealmResultsHandle extends ReleasableHandle { - RealmResultsHandle._(Pointer pointer) : super(pointer, 872); -} - -class RealmListHandle extends ReleasableHandle { - RealmListHandle._(Pointer pointer) : super(pointer, 88); -} - -class RealmQueryHandle extends ReleasableHandle { - RealmQueryHandle._(Pointer pointer) : super(pointer, 256); +class RealmResultsHandle extends RootedHandleBase { + RealmResultsHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 872); } -class ReleasableHandle extends HandleBase { - bool released = false; - ReleasableHandle(Pointer pointer, int size) : super(pointer, size); - void release() { - if (released) { - return; - } - _nativeFinalizer.detach(this); - _realmLib.realm_release(_pointer.cast()); - released = true; - if (_enableFinalizerTrace) _tearDownFinalizationTrace(this, _pointer); - } +class RealmListHandle extends RootedHandleBase { + RealmListHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 88); } -class RealmNotificationTokenHandle extends ReleasableHandle { - RealmNotificationTokenHandle._(Pointer pointer) : super(pointer, 32); +class _RealmQueryHandle extends RootedHandleBase { + _RealmQueryHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 256); } -class RealmCallbackTokenHandle extends ReleasableHandle { - RealmCallbackTokenHandle._(Pointer pointer) : super(pointer, 24); +class RealmNotificationTokenHandle extends RootedHandleBase { + RealmNotificationTokenHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 32); } class RealmCollectionChangesHandle extends HandleBase { @@ -2160,18 +2255,24 @@ class SubscriptionHandle extends HandleBase { SubscriptionHandle._(Pointer pointer) : super(pointer, 184); } -class SubscriptionSetHandle extends ReleasableHandle { - SubscriptionSetHandle._(Pointer pointer) : super(pointer, 128); +class SubscriptionSetHandle extends RootedHandleBase { + @override + bool get shouldRoot => true; + + SubscriptionSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 128); } class MutableSubscriptionSetHandle extends SubscriptionSetHandle { - MutableSubscriptionSetHandle._(Pointer pointer) : super._(pointer.cast()); + MutableSubscriptionSetHandle._(Pointer pointer, RealmHandle root) : super._(pointer.cast(), root); Pointer get _mutablePointer => super._pointer.cast(); } -class SessionHandle extends ReleasableHandle { - SessionHandle._(Pointer pointer) : super(pointer, 24); +class SessionHandle extends RootedHandleBase { + @override + bool get shouldRoot => true; + + SessionHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 24); } extension on List { diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index a7311edf8..dd71b1a69 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -40,6 +40,7 @@ export 'package:realm_common/realm_common.dart' MapTo, PrimaryKey, RealmError, + RealmClosedError, SyncError, SyncClientError, SyncClientResetError, @@ -251,17 +252,16 @@ class Realm implements Finalizable { /// All [RealmObject]s and `Realm ` collections are invalidated and can not be used. /// This method will not throw if called multiple times. void close() { - _syncSession?.handle.release(); - _syncSession = null; - - _subscriptions?.handle.release(); - _subscriptions = null; + if (isClosed) { + return; + } realmCore.closeRealm(this); + handle.release(); } /// Checks whether the `Realm` is closed. - bool get isClosed => realmCore.isRealmClosed(this); + bool get isClosed => _handle.released || realmCore.isRealmClosed(this); /// Fast lookup for a [RealmObject] with the specified [primaryKey]. T? find(Object? primaryKey) { @@ -318,7 +318,7 @@ class Realm implements Finalizable { return Realm._(config, realmCore.freeze(this)); } - SubscriptionSet? _subscriptions; + WeakReference? _subscriptions; /// The active [SubscriptionSet] for this [Realm] SubscriptionSet get subscriptions { @@ -326,12 +326,18 @@ class Realm implements Finalizable { throw RealmError('subscriptions is only valid on Realms opened with a FlexibleSyncConfiguration'); } - _subscriptions ??= SubscriptionSetInternal.create(this, realmCore.getSubscriptions(this)); - realmCore.refreshSubscriptionSet(_subscriptions!); - return _subscriptions!; + var result = _subscriptions?.target; + + if (result == null || result.handle.released) { + result = SubscriptionSetInternal.create(this, realmCore.getSubscriptions(this)); + realmCore.refreshSubscriptionSet(result); + _subscriptions = WeakReference(result); + } + + return result; } - Session? _syncSession; + WeakReference? _syncSession; /// The [Session] for this [Realm]. The sync session is responsible for two-way synchronization /// with MongoDB Atlas. If the [Realm] is not synchronized, accessing this property will throw. @@ -340,8 +346,14 @@ class Realm implements Finalizable { throw RealmError('session is only valid on synchronized Realms (i.e. opened with FlexibleSyncConfiguration)'); } - _syncSession ??= SessionInternal.create(realmCore.realmGetSession(this)); - return _syncSession!; + var result = _syncSession?.target; + + if (result == null || result.handle.released) { + result = SessionInternal.create(realmCore.realmGetSession(this)); + _syncSession = WeakReference(result); + } + + return result; } @override @@ -403,7 +415,13 @@ extension RealmInternal on Realm { } } - RealmHandle get handle => _handle; + RealmHandle get handle { + if (_handle.released) { + throw RealmClosedError('Cannot access realm that has been closed'); + } + + return _handle; + } static Realm getUnowned(Configuration config, RealmHandle handle, {bool isInMigration = false}) { return Realm._(config, handle, isInMigration); @@ -485,7 +503,12 @@ abstract class NotificationsController implements Finalizable { } void stop() { - handle?.release(); + // If handle is null or released, no-op + if (handle?.released != false) { + return; + } + + handle!.release(); handle = null; } } diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index 0767c39d8..e8129e2f9 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -404,8 +404,13 @@ extension RealmObjectInternal on RealmObject { return object; } - // if we ever see a _CastError here, we forgot to guard against misuse further up the call-stack - RealmObjectHandle get handle => _handle!; + RealmObjectHandle get handle { + if (_handle?.released == true) { + throw RealmClosedError('Cannot access an object that belongs to a closed Realm'); + } + + return _handle!; + } RealmAccessor get accessor => _accessor; } diff --git a/lib/src/results.dart b/lib/src/results.dart index 2a135cecf..c58e464a6 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -102,7 +102,13 @@ extension RealmResultsInternal on RealmResults { _handle.keepAlive(); } - RealmResultsHandle get handle => _handle; + RealmResultsHandle get handle { + if (_handle.released) { + throw RealmClosedError('Cannot access Results that belongs to a closed Realm'); + } + + return _handle; + } RealmObjectMetadata? get metadata => _metadata; diff --git a/lib/src/scheduler.dart b/lib/src/scheduler.dart index d4a5fd487..4f527971c 100644 --- a/lib/src/scheduler.dart +++ b/lib/src/scheduler.dart @@ -40,7 +40,12 @@ class Scheduler { } void stop() { + if (handle.released) { + return; + } + receivePort.close(); _receivePortFinalizer.detach(this); + handle.release(); } } diff --git a/lib/src/session.dart b/lib/src/session.dart index 08f9284e1..95ab634ab 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -158,7 +158,13 @@ extension SessionInternal on Session { static Session create(SessionHandle handle) => Session._(handle); - SessionHandle get handle => _handle; + SessionHandle get handle { + if (_handle.released) { + throw RealmClosedError('Cannot access a Session that belongs to a closed Realm'); + } + + return _handle; + } void raiseError(SyncErrorCategory category, int errorCode, bool isFatal) { realmCore.raiseError(this, category, errorCode, isFatal); diff --git a/lib/src/subscription.dart b/lib/src/subscription.dart index 3627db26b..f6ab0305e 100644 --- a/lib/src/subscription.dart +++ b/lib/src/subscription.dart @@ -217,7 +217,13 @@ extension SubscriptionSetInternal on SubscriptionSet { } Realm get realm => _realm; - SubscriptionSetHandle get handle => _handle; + SubscriptionSetHandle get handle { + if (_handle.released) { + throw RealmClosedError('Cannot access a SubscriptionSet that belongs to a closed Realm'); + } + + return _handle; + } static SubscriptionSet create(Realm realm, SubscriptionSetHandle handle) => ImmutableSubscriptionSet._(realm, handle); } diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 0a67b7b46..d4a397351 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -375,6 +375,26 @@ Future main([List? args]) async { expect(callbackEx.toString(), contains('The Realm is already in a write transaction')); }); + test("Configuration.initialDataCallback destroys objects after callback", () { + Exception? callbackEx; + late RealmResults people; + late Person george; + final config = Configuration.local([Person.schema], initialDataCallback: (realm) { + george = realm.add(Person('George')); + people = realm.all(); + expect(people.length, 1); + }); + + final realm = getRealm(config); + + expect(() => people.length, throws()); + expect(() => george.name, throws()); + expect(people.realm.isClosed, true); + + final peopleAagain = realm.all(); + expect(peopleAagain.length, 1); + }); + test('Configuration.shouldCompact can return false', () { var invoked = false; var config = Configuration.local([Dog.schema, Person.schema], shouldCompactCallback: (totalSize, usedSize) { diff --git a/test/list_test.dart b/test/list_test.dart index ba7be4311..36f0bb61d 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -217,43 +217,6 @@ Future main([List? args]) async { expect(allPlayers.length, 1); }); - test('List clear in closed realm - expected exception', () { - var config = Configuration.local([Team.schema, Person.schema]); - var realm = getRealm(config); - - //Create a team - var team = Team("TeamOne"); - realm.write(() => realm.add(team)); - - //Add the player to the team - realm.write(() => team.players.add(Person("Michael Schumacher"))); - - //Ensure teams and player are in realm - var teams = realm.all(); - expect(teams.length, 1); - expect(teams[0].players, isNotNull); - expect(teams[0].players.length, 1); - - var players = teams[0].players; - - realm.close(); - expect( - () => realm.write(() { - players.clear(); - }), - throws()); - - config = Configuration.local([Team.schema, Person.schema]); - realm = getRealm(config); - - //Teams must be reloaded since realm was reopened - teams = realm.all(); - - //Ensure that the team is still related to the player - expect(teams.length, 1); - expect(teams[0].players.length, 1); - }); - test('Read list property of a deleted object', () { var config = Configuration.local([Team.schema, Person.schema]); var realm = getRealm(config); diff --git a/test/migration_test.dart b/test/migration_test.dart index ceb3f1d44..2fcbcc724 100644 --- a/test/migration_test.dart +++ b/test/migration_test.dart @@ -24,6 +24,8 @@ import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; import 'test.dart'; import '../lib/src/results.dart'; +import '../lib/src/realm_object.dart'; +import '../lib/src/list.dart'; part 'migration_test.g.dart'; @@ -171,12 +173,6 @@ Future main([List? args]) async { newStudent.number = i; } - - // TODO: this is a hack to get the test working until https://github.com/realm/realm-dart/issues/527 is - // implemented. The issue is that the results are not released at the end of the migration, which will - // trigger the notification mechanics when the write transaction is committed. This will cause the queries - // to be reevaluated, which is no longer possible since the tables have changed and the columns don't match. - oldStudents.handle.release(); }); final v2Realm = getRealm(v2Config); @@ -413,4 +409,46 @@ Future main([List? args]) async { expect(dynamicRealm.schema.single.properties.length, 1); expect(dynamicRealm.dynamic.all('MyObject').single.dynamic.get('name'), 'name'); }); + + test("Migration doesn't hold on to handles", () { + final v1Config = Configuration.local([Person.schema, Team.schema], schemaVersion: 1); + final v1Realm = getRealm(v1Config); + + v1Realm.write(() { + v1Realm.add(Team('Lakers', players: [Person('Kobe')])); + }); + + v1Realm.close(); + + late RealmObject oldTeam; + late Team newTeam; + + late RealmResults oldTeams; + late RealmResults newTeams; + + late RealmList oldPlayers; + late RealmList newPlayers; + + final v2Config = Configuration.local([Person.schema, Team.schema], schemaVersion: 2, migrationCallback: (migration, oldSchemaVersion) { + oldTeams = migration.oldRealm.all('Team'); + newTeams = migration.newRealm.all(); + + oldTeam = oldTeams.single; + newTeam = newTeams.single; + + oldPlayers = oldTeam.dynamic.getList('players'); + newPlayers = newTeam.players; + }); + + final v2Realm = getRealm(v2Config); + + expect(() => oldTeam.handle.released, throws()); + expect(() => newTeam.handle.released, throws()); + + expect(() => oldTeams.handle.released, throws()); + expect(() => newTeams.handle.released, throws()); + + expect(() => oldPlayers.handle.released, throws()); + expect(() => newPlayers.handle.released, throws()); + }); } diff --git a/test/realm_object_test.dart b/test/realm_object_test.dart index a711e62c8..3f92f17b7 100644 --- a/test/realm_object_test.dart +++ b/test/realm_object_test.dart @@ -243,17 +243,6 @@ Future main([List? args]) async { expect(() => teamBeforeDelete.name, throws("Accessing object of type Team which has been invalidated or deleted")); }); - test('RealmObject - write object property after realm is closed', () { - var config = Configuration.local([Person.schema]); - var realm = getRealm(config); - - final person = Person('Markos'); - - realm.write(() => realm.add(person)); - realm.close(); - expect(() => realm.write(() => person.name = "Markos Sanches"), throws("Cannot access realm that has been closed")); - }); - test('RealmObject write deleted object property', () { var config = Configuration.local([Person.schema]); var realm = getRealm(config); diff --git a/test/realm_test.dart b/test/realm_test.dart index c8848ece7..134382514 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -264,14 +264,14 @@ Future main([List? args]) async { expect(realm.write(() => realm.add(carTwo, update: true)), carTwo); }); - test('Realm add object after realm is closed', () { + test('Realm write after realm is closed', () { var config = Configuration.local([Car.schema]); var realm = getRealm(config); final car = Car('Tesla'); realm.close(); - expect(() => realm.write(() => realm.add(car)), throws("Cannot access realm that has been closed")); + expect(() => realm.write(() {}), throws("Cannot access realm that has been closed")); }); test('Realm query', () { @@ -442,42 +442,6 @@ Future main([List? args]) async { expect(allPersons.length, 0); }); - test('Realm deleteMany from list after realm is closed', () { - var config = Configuration.local([Team.schema, Person.schema]); - var realm = getRealm(config); - - //Create a team - final team = Team("Ferrari"); - realm.write(() => realm.add(team)); - - //Add players to the team - final newPlayers = [ - Person("Michael Schumacher"), - Person("Sebastian Vettel"), - Person("Kimi Räikkönen"), - ]; - realm.write(() => team.players.addAll(newPlayers)); - - //Ensure team exists in realm - var teams = realm.all(); - expect(teams.length, 1); - - //Try to delete team players while realm is closed - final players = teams[0].players; - realm.close(); - expect( - () => realm.write(() { - realm.deleteMany(players); - }), - throws()); - - //Ensure all persons still exists in realm - config = Configuration.local([Team.schema, Person.schema]); - realm = getRealm(config); - final allPersons = realm.all(); - expect(allPersons.length, 3); - }); - test('Realm deleteMany from iterable', () { var config = Configuration.local([Team.schema, Person.schema]); var realm = getRealm(config); diff --git a/test/session_test.dart b/test/session_test.dart index fbb8823b4..b84ce5550 100644 --- a/test/session_test.dart +++ b/test/session_test.dart @@ -349,6 +349,20 @@ Future main([List? args]) async { await subscription.cancel(); }); + + baasTest('SyncSession when Realm is closed gets closed as well', (configuration) async { + final app = App(configuration); + final user = await getIntegrationUser(app); + final config = Configuration.flexibleSync(user, [Task.schema]); + final realm = getRealm(config); + + final session = realm.syncSession; + expect(() => session.state, returnsNormally); + + realm.close(); + + expect(() => session.state, throws()); + }); } class StreamProgressData { diff --git a/test/subscription_test.dart b/test/subscription_test.dart index 3995d4b2f..5fa3303ea 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -569,4 +569,18 @@ Future main([List? args]) async { expect(filtered, isNotEmpty); expect(filtered.length, all.length); }); + + baasTest('Subscriptions when realm is closed gets closed as well', (configuration) async { + final app = App(configuration); + final user = await getIntegrationUser(app); + + final config = Configuration.flexibleSync(user, [Task.schema]); + final realm = getRealm(config); + + final subscriptions = realm.subscriptions; + expect(() => subscriptions.state, returnsNormally); + + realm.close(); + expect(() => subscriptions.state, throws()); + }); }