diff --git a/CHANGELOG.md b/CHANGELOG.md index f77b5f849..0d20a256b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ ### Enhancements * Support setting `maxNumberOfActiveVersions` when creating a `Configuration`. ([#1036](https://github.com/realm/realm-dart/pull/1036)) * Add List.move extension method that moves an element from one index to another. Delegates to ManagedRealmList.move for managed lists. This allows notifications to correctly report moves, as opposed to reporting moves as deletes + inserts. ([#1037](https://github.com/realm/realm-dart/issues/1037)) +* Add `Realm.refresh()` and `Realm.refreshAsync()` support. ([#1046](https://github.com/realm/realm-dart/pull/1046)) * Support setting `shouldDeleteIfMigrationNeeded` when creating a `Configuration.local`. ([#1049](https://github.com/realm/realm-dart/issues/1049)) * Add `unknown` error code to all SyncErrors: `SyncSessionErrorCode.unknown`, `SyncConnectionErrorCode.unknown`, `SyncClientErrorCode.unknown`, `GeneralSyncErrorCode.unknown`. Use `unknown` error code instead of throwing a RealmError. ([#1052](https://github.com/realm/realm-dart/pull/1052)) * Add support for `RealmValue` data type. This new type can represent any valid Realm data type, including objects. Lists of `RealmValue` are also supported, but `RealmValue` itself cannot contain collections. Please note that a property of type `RealmValue` cannot be nullable, but can contain null, represented by the value `RealmValue.nullValue()`. ([#1051](https://github.com/realm/realm-dart/pull/1051)) diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index 2fa4d106a..b69620348 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -7786,6 +7786,23 @@ class RealmLibrary { ffi.Pointer, realm_on_collection_change_func_t)>(); + void realm_set_auto_refresh( + ffi.Pointer realm, + bool enable, + ) { + return _realm_set_auto_refresh( + realm, + enable, + ); + } + + late final _realm_set_auto_refreshPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Bool)>>('realm_set_auto_refresh'); + late final _realm_set_auto_refresh = _realm_set_auto_refreshPtr + .asFunction, bool)>(); + /// Clear a set of values. /// /// @return True if no exception occurred. diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index ea9c50333..593382f41 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -581,6 +581,10 @@ class _RealmCore { }); } + void realmSetAutoRefresh(Realm realm, bool enable) { + _realmLib.realm_set_auto_refresh(realm.handle._pointer, enable); + } + SchedulerHandle createScheduler(int isolateId, int sendPort) { final schedulerPtr = _realmLib.realm_dart_create_scheduler(isolateId, sendPort); return SchedulerHandle._(schedulerPtr); @@ -773,6 +777,29 @@ class _RealmCore { }); } + Future realmRefreshAsync(Realm realm) async { + final completer = Completer(); + final callback = Pointer.fromFunction)>(_realmRefreshAsyncCallback); + Pointer completerPtr = _realmLib.realm_dart_object_to_persistent_handle(completer); + Pointer result = _realmLib.realm_add_realm_refresh_callback( + realm.handle._pointer, callback.cast(), completerPtr, _realmLib.addresses.realm_dart_delete_persistent_handle); + + if (result == nullptr) { + return Future.value(false); + } + + return completer.future; + } + + static void _realmRefreshAsyncCallback(Pointer userdata) { + if (userdata == nullptr) { + return; + } + + final completer = _realmLib.realm_dart_persistent_handle_to_object(userdata) as Completer; + completer.complete(true); + } + RealmObjectMetadata getObjectMetadata(Realm realm, SchemaObject schema) { return using((Arena arena) { final found = arena(); diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index bd5a2987e..28343819a 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -562,6 +562,24 @@ class Realm implements Finalizable { realmCore.writeCopy(this, config); } + + /// Update the `Realm` instance and outstanding objects to point to the most recent persisted version. + /// + /// If another process or [Isolate] has made changes to the realm file, this causes + /// those changes to become visible in this realm instance. + /// Typically you don't need to call this method since Realm has auto-refresh built-in. + /// Note that this may return `true` even if no data has actually changed. + bool refresh() { + return realmCore.realmRefresh(this); + } + + /// Returns a [Future] that will complete when the `Realm` is refreshed to the version which is the + /// latest version at the time when this method is called. + /// + /// Note that this may return `true` even if no data has actually changed. + Future refreshAsync() async { + return realmCore.realmRefreshAsync(this); + } } /// Provides a scope to safely write data to a [Realm]. Can be created using [Realm.beginWrite] or @@ -726,6 +744,12 @@ extension RealmInternal on Realm { addUnmanagedRealmObjectFromValue(value.value, update); } } + + // Internal method that prevents the realm from being automatically refreshed. + // This method is used for testing purposes only. + void disableAutoRefreshForTesting() { + realmCore.realmSetAutoRefresh(this, false); + } } /// @nodoc diff --git a/src/realm_dart.cpp b/src/realm_dart.cpp index 58bef2abe..dd70ebddd 100644 --- a/src/realm_dart.cpp +++ b/src/realm_dart.cpp @@ -115,3 +115,7 @@ RLM_API void realm_dettach_finalizer(void* finalizableHandle, Dart_Handle handle Dart_FinalizableHandle finalHandle = reinterpret_cast(finalizableHandle); return Dart_DeleteFinalizableHandle_DL(finalHandle, handle); } + +RLM_API void realm_set_auto_refresh(realm_t* realm, bool enable){ + (*realm)->set_auto_refresh(enable); +} diff --git a/src/realm_dart.h b/src/realm_dart.h index 99fd53e72..aa179796f 100644 --- a/src/realm_dart.h +++ b/src/realm_dart.h @@ -48,6 +48,5 @@ RLM_API const char* realm_dart_library_version(); RLM_API void* realm_attach_finalizer(Dart_Handle handle, void* realmPtr, int size); RLM_API void realm_dettach_finalizer(void* finalizableHandle, Dart_Handle handle); - - +RLM_API void realm_set_auto_refresh(realm_t* realm, bool enable); #endif // REALM_DART_H \ No newline at end of file diff --git a/src/realm_dart.hpp b/src/realm_dart.hpp index eb398db7e..98b118e97 100644 --- a/src/realm_dart.hpp +++ b/src/realm_dart.hpp @@ -23,7 +23,6 @@ #include struct realm_dart_userdata_async { -public: realm_dart_userdata_async(Dart_Handle handle, void* callback, realm_scheduler_t* scheduler) : handle(Dart_NewPersistentHandle_DL(handle)) , dart_callback(callback) diff --git a/test/realm_test.dart b/test/realm_test.dart index 3b9483b13..e952a2e34 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -18,6 +18,7 @@ // ignore_for_file: unused_local_variable, avoid_relative_lib_imports +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; @@ -27,6 +28,7 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:path/path.dart' as p; import 'package:cancellation_token/cancellation_token.dart'; import '../lib/realm.dart'; +import '../lib/src/realm_class.dart' as realmInternal; import 'test.dart'; Future main([List? args]) async { @@ -1582,22 +1584,19 @@ Future main([List? args]) async { final pathCopy = originalConfig.path.replaceFirst(p.basenameWithoutExtension(originalConfig.path), generateRandomString(10)); final configCopy = Configuration.local([Car.schema], path: pathCopy); originalRealm.write(() { - expect(() => originalRealm.writeCopy(configCopy), - throws("Copying a Realm is not allowed within a write transaction or during migration.")); + expect(() => originalRealm.writeCopy(configCopy), throws("Copying a Realm is not allowed within a write transaction or during migration.")); }); originalRealm.close(); }); - test('Realm writeCopy Local->Local during migration is mot allowed', () { + test('Realm writeCopy Local->Local during migration is not allowed', () { getRealm(Configuration.local([Car.schema], schemaVersion: 1)).close(); final configWithMigrationCallback = Configuration.local([Car.schema], schemaVersion: 2, migrationCallback: (migration, oldVersion) { - final pathCopy = migration.newRealm.config.path.replaceFirst(p.basenameWithoutExtension(migration.newRealm.config.path), generateRandomString(10)); final configCopy = Configuration.local([Car.schema], path: pathCopy); - expect(() => migration.newRealm.writeCopy(configCopy), - throws("Copying a Realm is not allowed within a write transaction or during migration.")); - + expect( + () => migration.newRealm.writeCopy(configCopy), throws("Copying a Realm is not allowed within a write transaction or during migration.")); }); getRealm(configWithMigrationCallback); }); @@ -1756,6 +1755,94 @@ Future main([List? args]) async { }); } } + test('Realm.refresh no changes', () async { + final realm = getRealm(Configuration.local([Person.schema])); + final result = realm.refresh(); + expect(result, false); + }); + + test('Realm.refreshAsync() sync transaction', () async { + final realm = getRealm(Configuration.local([Person.schema])); + var called = false; + bool isRefreshed = false; + final transaction = realm.beginWrite(); + realm.refreshAsync().then((refreshed) { + called = true; + isRefreshed = refreshed; + }); + realm.add(Person("name")); + transaction.commit(); + + await Future.delayed(Duration(milliseconds: 1)); + expect(isRefreshed, false); + expect(called, true); + expect(realm.all().length, 1); + }); + + test('Realm.refreshAsync from within a write block', () async { + final realm = getRealm(Configuration.local([Person.schema])); + var called = false; + bool isRefreshed = false; + realm.write(() { + realm.refreshAsync().then((refreshed) { + called = true; + isRefreshed = refreshed; + }); + realm.add(Person("name")); + }); + + await Future.delayed(Duration(milliseconds: 1)); + expect(isRefreshed, false); + expect(called, true); + expect(realm.all().length, 1); + }); + + test('Realm.refreshAsync from within an async transaction', () async { + final realm = getRealm(Configuration.local([Person.schema])); + bool called = false; + bool isRefreshed = false; + final transaction = await realm.beginWriteAsync(); + realm.refreshAsync().then((refreshed) { + called = true; + isRefreshed = refreshed; + }); + realm.add(Person("name")); + await transaction.commitAsync(); + expect(isRefreshed, false); + expect(called, true); + expect(realm.all().length, 1); + }); + + test('Realm.refresh on frozen realm should be no-op', () async { + var realm = getRealm(Configuration.local([Person.schema])); + bool called = false; + realm = realm.freeze(); + expect(realm.refresh(), false); + }); + + test('Realm.refresh', () async { + final realm = getRealm(Configuration.local([Person.schema])); + String personName = generateRandomString(5); + final path = realm.config.path; + final results = realm.query(r"name == $0", [personName]); + + expect(realm.refresh(), false); + realm.disableAutoRefreshForTesting(); + + ReceivePort receivePort = ReceivePort(); + Isolate.spawn((SendPort sendPort) async { + final externalRealm = Realm(Configuration.local([Person.schema], path: path)); + externalRealm.write(() => externalRealm.add(Person(personName))); + externalRealm.close(); + sendPort.send(true); + }, receivePort.sendPort); + + await receivePort.first; + expect(results.length, 0); + expect(realm.refresh(), true); + expect(results.length, 1); + receivePort.close(); + }); } List generateEncryptionKey() {