From 037e3faba8000ba1a1a061fde58f4c2aaceb0a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Mon, 30 Oct 2023 16:44:05 +0100 Subject: [PATCH 01/16] kn/fix skip then iterate bug (#1410) --- CHANGELOG.md | 2 +- lib/src/results.dart | 5 +++-- test/results_test.dart | 16 +++++++++++++++- test/test.dart | 3 +++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee256831d..2be03650b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * None ### Fixed -* None +* Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) ### Compatibility * Realm Studio: 13.0.0 or later. diff --git a/lib/src/results.dart b/lib/src/results.dart index 4bea432f3d..160c49a927 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -86,7 +86,7 @@ class RealmResults extends Iterable with RealmEntity imple var results = this; if (_supportsSnapshot) { final handle = realmCore.resultsSnapshot(this); - results = RealmResultsInternal.create(handle, realm, _metadata); + results = RealmResultsInternal.create(handle, realm, _metadata, _skipOffset); } return _RealmResultsIterator(results); } @@ -184,8 +184,9 @@ extension RealmResultsInternal on RealmResults { RealmResultsHandle handle, Realm realm, RealmObjectMetadata? metadata, + [int skip = 0] ) => - RealmResults._(handle, realm, metadata); + RealmResults._(handle, realm, metadata, skip); } /// Describes the changes in a Realm results collection since the last time the notification callback was invoked. diff --git a/test/results_test.dart b/test/results_test.dart index 61b7d562cb..9a1115b28c 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -1008,9 +1008,23 @@ Future main([List? args]) async { for (int i = 0; i < max; i++) { for (var j = 0; j < max - i; j++) { - expect(all.skip(i + j).contains(tasks[i + j]), true); + expect(all.skip(i).contains(tasks[i + j]), true); expect(all.skip(i + j + 1).contains(tasks[i + j]), false); } } }); + + test('RealmResults.skip().take()', () { + final config = Configuration.local([Task.schema]); + final realm = getRealm(config); + const max = 10; + realm.write(() { + realm.addAll(List.generate(max, (_) => Task(ObjectId()))); + }); + + final results = realm.all(); + + expect(results.skip(2), results.toList().sublist(2)); + expect(results.skip(2).take(3), [results[2], results[3], results[4]]); + }); } diff --git a/test/test.dart b/test/test.dart index 093b7576b7..da73cca3bc 100644 --- a/test/test.dart +++ b/test/test.dart @@ -97,6 +97,9 @@ class _Task { @PrimaryKey() @MapTo('_id') late ObjectId id; + + @override + String toString() => 'Task($id)'; } @RealmModel() From d5aaf25ef036e2b6cd9c29cc4337903294fe6dec Mon Sep 17 00:00:00 2001 From: Maxim <33198447+Pluxury@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:45:18 +0300 Subject: [PATCH 02/16] add 'ignore_for_file: type=lint' to *.g.dart files (#1413) Co-authored-by: max_merkulov --- CHANGELOG.md | 2 +- example/bin/myapp.g.dart | 2 ++ flutter/realm_flutter/example/lib/main.g.dart | 2 ++ generator/lib/src/realm_model_info.dart | 1 + generator/test/good_test.dart | 1 + .../test/good_test_data/all_types.expected | 3 +++ .../good_test_data/asymmetric_object.expected | 2 ++ .../test/good_test_data/binary_type.expected | 1 + .../embedded_annotations.expected | 2 ++ .../good_test_data/embedded_objects.expected | 3 +++ .../good_test_data/indexable_types.expected | 1 + .../list_initialization.expected | 1 + generator/test/good_test_data/mapto.expected | 1 + .../good_test_data/optional_argument.expected | 1 + .../test/good_test_data/pinhole.expected | 1 + .../test/good_test_data/primary_key.expected | 8 ++++++ .../test/good_test_data/realm_set.expected | 2 ++ .../good_test_data/required_argument.expected | 1 + ...uired_argument_with_default_value.expected | 1 + .../user_defined_getter.expected | 1 + test/asymmetric_test.g.dart | 3 +++ test/backlinks_test.g.dart | 2 ++ test/indexed_test.g.dart | 3 +++ test/migration_test.g.dart | 5 ++++ test/realm_object_test.g.dart | 10 +++++++ test/realm_set_test.g.dart | 2 ++ test/realm_value_test.g.dart | 3 +++ test/test.g.dart | 27 +++++++++++++++++++ 28 files changed, 91 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be03650b5..0e84d32126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## vNext (TBD) ### Enhancements -* None +* Suppressing rules for a *.g.dart files ([#1413](https://github.com/realm/realm-dart/pull/1413)) ### Fixed * Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) diff --git a/example/bin/myapp.g.dart b/example/bin/myapp.g.dart index 6ea6cfb71a..4b81d0787d 100644 --- a/example/bin/myapp.g.dart +++ b/example/bin/myapp.g.dart @@ -6,6 +6,7 @@ part of 'myapp.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; @@ -70,6 +71,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/flutter/realm_flutter/example/lib/main.g.dart b/flutter/realm_flutter/example/lib/main.g.dart index f7627287f4..ba66902880 100644 --- a/flutter/realm_flutter/example/lib/main.g.dart +++ b/flutter/realm_flutter/example/lib/main.g.dart @@ -6,6 +6,7 @@ part of 'main.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; @@ -70,6 +71,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/generator/lib/src/realm_model_info.dart b/generator/lib/src/realm_model_info.dart index 84843ae4c8..1d58c78312 100644 --- a/generator/lib/src/realm_model_info.dart +++ b/generator/lib/src/realm_model_info.dart @@ -32,6 +32,7 @@ class RealmModelInfo { const RealmModelInfo(this.name, this.modelName, this.realmName, this.fields, this.baseType); Iterable toCode() sync* { + yield '// ignore_for_file: type=lint'; yield 'class $name extends $modelName with RealmEntity, RealmObjectBase, ${baseType.className} {'; { final allSettable = fields.where((f) => !f.type.isRealmCollection && !f.isRealmBacklink).toList(); diff --git a/generator/test/good_test.dart b/generator/test/good_test.dart index bc0e3a6f91..62446b4813 100644 --- a/generator/test/good_test.dart +++ b/generator/test/good_test.dart @@ -36,6 +36,7 @@ class _MappedToo { // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class MappedToo extends _MappedToo with RealmEntity, RealmObjectBase, RealmObject { MappedToo({ diff --git a/generator/test/good_test_data/all_types.expected b/generator/test/good_test_data/all_types.expected index 385bef5b2e..87389202f1 100644 --- a/generator/test/good_test_data/all_types.expected +++ b/generator/test/good_test_data/all_types.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; @@ -49,6 +50,7 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; @@ -225,6 +227,7 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class PrimitiveTypes extends _PrimitiveTypes with RealmEntity, RealmObjectBase, RealmObject { PrimitiveTypes( diff --git a/generator/test/good_test_data/asymmetric_object.expected b/generator/test/good_test_data/asymmetric_object.expected index f96dbfde20..eaf9e1d81a 100644 --- a/generator/test/good_test_data/asymmetric_object.expected +++ b/generator/test/good_test_data/asymmetric_object.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Asymmetric extends _Asymmetric with RealmEntity, RealmObjectBase, RealmObject { Asymmetric( @@ -70,6 +71,7 @@ class Asymmetric extends _Asymmetric } } +// ignore_for_file: type=lint class Embedded extends _Embedded with RealmEntity, RealmObjectBase, EmbeddedObject { Embedded( diff --git a/generator/test/good_test_data/binary_type.expected b/generator/test/good_test_data/binary_type.expected index b1b8502f0d..f9d166c360 100644 --- a/generator/test/good_test_data/binary_type.expected +++ b/generator/test/good_test_data/binary_type.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Foo( Uint8List requiredBinaryProp, { diff --git a/generator/test/good_test_data/embedded_annotations.expected b/generator/test/good_test_data/embedded_annotations.expected index 9d57b36ea4..ed4f5468d0 100644 --- a/generator/test/good_test_data/embedded_annotations.expected +++ b/generator/test/good_test_data/embedded_annotations.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Parent extends _Parent with RealmEntity, RealmObjectBase, RealmObject { Parent({ Child1? child, @@ -50,6 +51,7 @@ class Parent extends _Parent with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Child1 extends _Child1 with RealmEntity, RealmObjectBase, EmbeddedObject { Child1( String value, diff --git a/generator/test/good_test_data/embedded_objects.expected b/generator/test/good_test_data/embedded_objects.expected index efff5f50f2..c220d51bd9 100644 --- a/generator/test/good_test_data/embedded_objects.expected +++ b/generator/test/good_test_data/embedded_objects.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Parent extends _Parent with RealmEntity, RealmObjectBase, RealmObject { Parent({ Child1? child, @@ -47,6 +48,7 @@ class Parent extends _Parent with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Child1 extends _Child1 with RealmEntity, RealmObjectBase, EmbeddedObject { Child1( String value, { @@ -111,6 +113,7 @@ class Child1 extends _Child1 with RealmEntity, RealmObjectBase, EmbeddedObject { } } +// ignore_for_file: type=lint class Child2 extends _Child2 with RealmEntity, RealmObjectBase, EmbeddedObject { Child2( bool boolProp, diff --git a/generator/test/good_test_data/indexable_types.expected b/generator/test/good_test_data/indexable_types.expected index d42d0e3d0a..5b4e218d02 100644 --- a/generator/test/good_test_data/indexable_types.expected +++ b/generator/test/good_test_data/indexable_types.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Indexable extends _Indexable with RealmEntity, RealmObjectBase, RealmObject { Indexable( diff --git a/generator/test/good_test_data/list_initialization.expected b/generator/test/good_test_data/list_initialization.expected index 5fcfb33b78..331621f544 100644 --- a/generator/test/good_test_data/list_initialization.expected +++ b/generator/test/good_test_data/list_initialization.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person({ Iterable children = const [], diff --git a/generator/test/good_test_data/mapto.expected b/generator/test/good_test_data/mapto.expected index 45fa668cf7..dc1876955e 100644 --- a/generator/test/good_test_data/mapto.expected +++ b/generator/test/good_test_data/mapto.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Original extends $Original with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/generator/test/good_test_data/optional_argument.expected b/generator/test/good_test_data/optional_argument.expected index fb7565b4f9..f42fb6b3ea 100644 --- a/generator/test/good_test_data/optional_argument.expected +++ b/generator/test/good_test_data/optional_argument.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person({ Person? spouse, diff --git a/generator/test/good_test_data/pinhole.expected b/generator/test/good_test_data/pinhole.expected index fcc868bcbc..9062dc8add 100644 --- a/generator/test/good_test_data/pinhole.expected +++ b/generator/test/good_test_data/pinhole.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/generator/test/good_test_data/primary_key.expected b/generator/test/good_test_data/primary_key.expected index fc18b7497d..50706082a9 100644 --- a/generator/test/good_test_data/primary_key.expected +++ b/generator/test/good_test_data/primary_key.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class IntPK extends _IntPK with RealmEntity, RealmObjectBase, RealmObject { IntPK( int id, @@ -33,6 +34,7 @@ class IntPK extends _IntPK with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class NullableIntPK extends _NullableIntPK with RealmEntity, RealmObjectBase, RealmObject { NullableIntPK( @@ -67,6 +69,7 @@ class NullableIntPK extends _NullableIntPK } } +// ignore_for_file: type=lint class StringPK extends _StringPK with RealmEntity, RealmObjectBase, RealmObject { StringPK( @@ -99,6 +102,7 @@ class StringPK extends _StringPK } } +// ignore_for_file: type=lint class NullableStringPK extends _NullableStringPK with RealmEntity, RealmObjectBase, RealmObject { NullableStringPK( @@ -134,6 +138,7 @@ class NullableStringPK extends _NullableStringPK } } +// ignore_for_file: type=lint class ObjectIdPK extends _ObjectIdPK with RealmEntity, RealmObjectBase, RealmObject { ObjectIdPK( @@ -166,6 +171,7 @@ class ObjectIdPK extends _ObjectIdPK } } +// ignore_for_file: type=lint class NullableObjectIdPK extends _NullableObjectIdPK with RealmEntity, RealmObjectBase, RealmObject { NullableObjectIdPK( @@ -201,6 +207,7 @@ class NullableObjectIdPK extends _NullableObjectIdPK } } +// ignore_for_file: type=lint class UuidPK extends _UuidPK with RealmEntity, RealmObjectBase, RealmObject { UuidPK( Uuid id, @@ -232,6 +239,7 @@ class UuidPK extends _UuidPK with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class NullableUuidPK extends _NullableUuidPK with RealmEntity, RealmObjectBase, RealmObject { NullableUuidPK( diff --git a/generator/test/good_test_data/realm_set.expected b/generator/test/good_test_data/realm_set.expected index 3bb5a29629..0ccc439c50 100644 --- a/generator/test/good_test_data/realm_set.expected +++ b/generator/test/good_test_data/realm_set.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Car( String make, @@ -33,6 +34,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class RealmSets extends _RealmSets with RealmEntity, RealmObjectBase, RealmObject { RealmSets( diff --git a/generator/test/good_test_data/required_argument.expected b/generator/test/good_test_data/required_argument.expected index ba7db413e3..525c547227 100644 --- a/generator/test/good_test_data/required_argument.expected +++ b/generator/test/good_test_data/required_argument.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person( String name, diff --git a/generator/test/good_test_data/required_argument_with_default_value.expected b/generator/test/good_test_data/required_argument_with_default_value.expected index 5ea8348de2..f769340502 100644 --- a/generator/test/good_test_data/required_argument_with_default_value.expected +++ b/generator/test/good_test_data/required_argument_with_default_value.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/generator/test/good_test_data/user_defined_getter.expected b/generator/test/good_test_data/user_defined_getter.expected index b9506bae90..a2e42eefed 100644 --- a/generator/test/good_test_data/user_defined_getter.expected +++ b/generator/test/good_test_data/user_defined_getter.expected @@ -2,6 +2,7 @@ // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person( String name, diff --git a/test/asymmetric_test.g.dart b/test/asymmetric_test.g.dart index 286a61e5d9..41344b439a 100644 --- a/test/asymmetric_test.g.dart +++ b/test/asymmetric_test.g.dart @@ -6,6 +6,7 @@ part of 'asymmetric_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Asymmetric extends _Asymmetric with RealmEntity, RealmObjectBase, AsymmetricObject { Asymmetric( @@ -53,6 +54,7 @@ class Asymmetric extends _Asymmetric } } +// ignore_for_file: type=lint class Embedded extends _Embedded with RealmEntity, RealmObjectBase, EmbeddedObject { Embedded( @@ -105,6 +107,7 @@ class Embedded extends _Embedded } } +// ignore_for_file: type=lint class Symmetric extends _Symmetric with RealmEntity, RealmObjectBase, RealmObject { Symmetric( diff --git a/test/backlinks_test.g.dart b/test/backlinks_test.g.dart index 48fa00b8af..224698d8f2 100644 --- a/test/backlinks_test.g.dart +++ b/test/backlinks_test.g.dart @@ -6,6 +6,7 @@ part of 'backlinks_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; @@ -67,6 +68,7 @@ class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Target extends _Target with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/test/indexed_test.g.dart b/test/indexed_test.g.dart index 18d108a859..a29ad252b2 100644 --- a/test/indexed_test.g.dart +++ b/test/indexed_test.g.dart @@ -6,6 +6,7 @@ part of 'indexed_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class WithIndexes extends _WithIndexes with RealmEntity, RealmObjectBase, RealmObject { WithIndexes( @@ -88,6 +89,7 @@ class WithIndexes extends _WithIndexes } } +// ignore_for_file: type=lint class NoIndexes extends _NoIndexes with RealmEntity, RealmObjectBase, RealmObject { NoIndexes( @@ -163,6 +165,7 @@ class NoIndexes extends _NoIndexes } } +// ignore_for_file: type=lint class ObjectWithFTSIndex extends _ObjectWithFTSIndex with RealmEntity, RealmObjectBase, RealmObject { ObjectWithFTSIndex( diff --git a/test/migration_test.g.dart b/test/migration_test.g.dart index 7cb6db9e91..a7b35c72cd 100644 --- a/test/migration_test.g.dart +++ b/test/migration_test.g.dart @@ -6,6 +6,7 @@ part of 'migration_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class PersonIntName extends _PersonIntName with RealmEntity, RealmObjectBase, RealmObject { PersonIntName( @@ -38,6 +39,7 @@ class PersonIntName extends _PersonIntName } } +// ignore_for_file: type=lint class StudentV1 extends _StudentV1 with RealmEntity, RealmObjectBase, RealmObject { StudentV1( @@ -79,6 +81,7 @@ class StudentV1 extends _StudentV1 } } +// ignore_for_file: type=lint class MyObjectWithTypo extends _MyObjectWithTypo with RealmEntity, RealmObjectBase, RealmObject { MyObjectWithTypo( @@ -121,6 +124,7 @@ class MyObjectWithTypo extends _MyObjectWithTypo } } +// ignore_for_file: type=lint class MyObjectWithoutTypo extends _MyObjectWithoutTypo with RealmEntity, RealmObjectBase, RealmObject { MyObjectWithoutTypo( @@ -163,6 +167,7 @@ class MyObjectWithoutTypo extends _MyObjectWithoutTypo } } +// ignore_for_file: type=lint class MyObjectWithoutValue extends _MyObjectWithoutValue with RealmEntity, RealmObjectBase, RealmObject { MyObjectWithoutValue( diff --git a/test/realm_object_test.g.dart b/test/realm_object_test.g.dart index 39623e31d9..8e77e81b97 100644 --- a/test/realm_object_test.g.dart +++ b/test/realm_object_test.g.dart @@ -6,6 +6,7 @@ part of 'realm_object_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class ObjectIdPrimaryKey extends _ObjectIdPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { ObjectIdPrimaryKey( @@ -40,6 +41,7 @@ class ObjectIdPrimaryKey extends _ObjectIdPrimaryKey } } +// ignore_for_file: type=lint class NullableObjectIdPrimaryKey extends _NullableObjectIdPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { NullableObjectIdPrimaryKey( @@ -75,6 +77,7 @@ class NullableObjectIdPrimaryKey extends _NullableObjectIdPrimaryKey } } +// ignore_for_file: type=lint class IntPrimaryKey extends _IntPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { IntPrimaryKey( @@ -108,6 +111,7 @@ class IntPrimaryKey extends _IntPrimaryKey } } +// ignore_for_file: type=lint class NullableIntPrimaryKey extends _NullableIntPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { NullableIntPrimaryKey( @@ -143,6 +147,7 @@ class NullableIntPrimaryKey extends _NullableIntPrimaryKey } } +// ignore_for_file: type=lint class StringPrimaryKey extends _StringPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { StringPrimaryKey( @@ -177,6 +182,7 @@ class StringPrimaryKey extends _StringPrimaryKey } } +// ignore_for_file: type=lint class NullableStringPrimaryKey extends _NullableStringPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { NullableStringPrimaryKey( @@ -212,6 +218,7 @@ class NullableStringPrimaryKey extends _NullableStringPrimaryKey } } +// ignore_for_file: type=lint class UuidPrimaryKey extends _UuidPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { UuidPrimaryKey( @@ -245,6 +252,7 @@ class UuidPrimaryKey extends _UuidPrimaryKey } } +// ignore_for_file: type=lint class NullableUuidPrimaryKey extends _NullableUuidPrimaryKey with RealmEntity, RealmObjectBase, RealmObject { NullableUuidPrimaryKey( @@ -280,6 +288,7 @@ class NullableUuidPrimaryKey extends _NullableUuidPrimaryKey } } +// ignore_for_file: type=lint class RemappedFromAnotherFile extends _RemappedFromAnotherFile with RealmEntity, RealmObjectBase, RealmObject { RemappedFromAnotherFile({ @@ -320,6 +329,7 @@ class RemappedFromAnotherFile extends _RemappedFromAnotherFile } } +// ignore_for_file: type=lint class BoolValue extends _BoolValue with RealmEntity, RealmObjectBase, RealmObject { BoolValue( diff --git a/test/realm_set_test.g.dart b/test/realm_set_test.g.dart index 8aea06a0f7..c8ce0aaa1b 100644 --- a/test/realm_set_test.g.dart +++ b/test/realm_set_test.g.dart @@ -6,6 +6,7 @@ part of 'realm_set_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Car( String make, { @@ -45,6 +46,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class TestRealmSets extends _TestRealmSets with RealmEntity, RealmObjectBase, RealmObject { TestRealmSets( diff --git a/test/realm_value_test.g.dart b/test/realm_value_test.g.dart index 44976478b9..0aaec8d534 100644 --- a/test/realm_value_test.g.dart +++ b/test/realm_value_test.g.dart @@ -6,6 +6,7 @@ part of 'realm_value_test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class TuckedIn extends _TuckedIn with RealmEntity, RealmObjectBase, EmbeddedObject { static var _defaultsSet = false; @@ -45,6 +46,7 @@ class TuckedIn extends _TuckedIn } } +// ignore_for_file: type=lint class AnythingGoes extends _AnythingGoes with RealmEntity, RealmObjectBase, RealmObject { AnythingGoes({ @@ -92,6 +94,7 @@ class AnythingGoes extends _AnythingGoes } } +// ignore_for_file: type=lint class Stuff extends _Stuff with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; diff --git a/test/test.g.dart b/test/test.g.dart index 4980962f72..b5d1e074b6 100644 --- a/test/test.g.dart +++ b/test/test.g.dart @@ -6,6 +6,7 @@ part of 'test.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Car( String make, @@ -37,6 +38,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person( String name, @@ -68,6 +70,7 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Dog extends _Dog with RealmEntity, RealmObjectBase, RealmObject { Dog( String name, { @@ -117,6 +120,7 @@ class Dog extends _Dog with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Team extends _Team with RealmEntity, RealmObjectBase, RealmObject { Team( String name, { @@ -171,6 +175,7 @@ class Team extends _Team with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Student extends _Student with RealmEntity, RealmObjectBase, RealmObject { Student( int number, { @@ -229,6 +234,7 @@ class Student extends _Student with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class School extends _School with RealmEntity, RealmObjectBase, RealmObject { School( String name, { @@ -303,6 +309,7 @@ class School extends _School with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class RemappedClass extends $RemappedClass with RealmEntity, RealmObjectBase, RealmObject { RemappedClass( @@ -354,6 +361,7 @@ class RemappedClass extends $RemappedClass } } +// ignore_for_file: type=lint class Task extends _Task with RealmEntity, RealmObjectBase, RealmObject { Task( ObjectId id, @@ -386,6 +394,7 @@ class Task extends _Task with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Product extends _Product with RealmEntity, RealmObjectBase, RealmObject { Product( ObjectId id, @@ -429,6 +438,7 @@ class Product extends _Product with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Schedule extends _Schedule with RealmEntity, RealmObjectBase, RealmObject { Schedule( @@ -473,6 +483,7 @@ class Schedule extends _Schedule } } +// ignore_for_file: type=lint class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Foo( Uint8List requiredBinaryProp, { @@ -529,6 +540,7 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class AllTypes extends _AllTypes with RealmEntity, RealmObjectBase, RealmObject { AllTypes( @@ -735,6 +747,7 @@ class AllTypes extends _AllTypes } } +// ignore_for_file: type=lint class LinksClass extends _LinksClass with RealmEntity, RealmObjectBase, RealmObject { LinksClass( @@ -791,6 +804,7 @@ class LinksClass extends _LinksClass } } +// ignore_for_file: type=lint class AllCollections extends _AllCollections with RealmEntity, RealmObjectBase, RealmObject { AllCollections({ @@ -1010,6 +1024,7 @@ class AllCollections extends _AllCollections } } +// ignore_for_file: type=lint class NullableTypes extends _NullableTypes with RealmEntity, RealmObjectBase, RealmObject { NullableTypes( @@ -1129,6 +1144,7 @@ class NullableTypes extends _NullableTypes } } +// ignore_for_file: type=lint class Event extends _Event with RealmEntity, RealmObjectBase, RealmObject { Event( ObjectId id, { @@ -1204,6 +1220,7 @@ class Event extends _Event with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Party extends _Party with RealmEntity, RealmObjectBase, RealmObject { Party( int year, { @@ -1266,6 +1283,7 @@ class Party extends _Party with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Friend extends _Friend with RealmEntity, RealmObjectBase, RealmObject { static var _defaultsSet = false; @@ -1335,6 +1353,7 @@ class Friend extends _Friend with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class When extends _When with RealmEntity, RealmObjectBase, RealmObject { When( DateTime dateTimeUtc, @@ -1378,6 +1397,7 @@ class When extends _When with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Player extends _Player with RealmEntity, RealmObjectBase, RealmObject { Player( String name, { @@ -1430,6 +1450,7 @@ class Player extends _Player with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class Game extends _Game with RealmEntity, RealmObjectBase, RealmObject { Game({ Iterable winnerByRound = const [], @@ -1465,6 +1486,7 @@ class Game extends _Game with RealmEntity, RealmObjectBase, RealmObject { } } +// ignore_for_file: type=lint class AllTypesEmbedded extends _AllTypesEmbedded with RealmEntity, RealmObjectBase, EmbeddedObject { AllTypesEmbedded( @@ -1745,6 +1767,7 @@ class AllTypesEmbedded extends _AllTypesEmbedded } } +// ignore_for_file: type=lint class ObjectWithEmbedded extends _ObjectWithEmbedded with RealmEntity, RealmObjectBase, RealmObject { ObjectWithEmbedded( @@ -1842,6 +1865,7 @@ class ObjectWithEmbedded extends _ObjectWithEmbedded } } +// ignore_for_file: type=lint class RecursiveEmbedded1 extends _RecursiveEmbedded1 with RealmEntity, RealmObjectBase, EmbeddedObject { RecursiveEmbedded1( @@ -1914,6 +1938,7 @@ class RecursiveEmbedded1 extends _RecursiveEmbedded1 } } +// ignore_for_file: type=lint class RecursiveEmbedded2 extends _RecursiveEmbedded2 with RealmEntity, RealmObjectBase, EmbeddedObject { RecursiveEmbedded2( @@ -1986,6 +2011,7 @@ class RecursiveEmbedded2 extends _RecursiveEmbedded2 } } +// ignore_for_file: type=lint class RecursiveEmbedded3 extends _RecursiveEmbedded3 with RealmEntity, RealmObjectBase, EmbeddedObject { RecursiveEmbedded3( @@ -2020,6 +2046,7 @@ class RecursiveEmbedded3 extends _RecursiveEmbedded3 } } +// ignore_for_file: type=lint class ObjectWithDecimal extends _ObjectWithDecimal with RealmEntity, RealmObjectBase, RealmObject { ObjectWithDecimal( From 5db8f27f17409e42bc3ef20c01800b0015bc9025 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 31 Oct 2023 16:26:34 +0200 Subject: [PATCH 03/16] Fix RealmObject.hashCode (#1420) --- CHANGELOG.md | 1 + lib/src/native/realm_core.dart | 9 ++++ lib/src/realm_object.dart | 12 +++++ test/realm_object_test.dart | 99 +++++++++++++++++++++++++++++++++- test/subscription_test.dart | 2 +- 5 files changed, 121 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e84d32126..4a47287913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed * Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) +* Fixed RealmObject not overriding `hashCode`, which would lead to sets of RealmObjects potentially containing duplicates. ([#1418](https://github.com/realm/realm-dart/issues/1418)) ### Compatibility * Realm Studio: 13.0.0 or later. diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 81cce9cdc1..df0e985820 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -1490,6 +1490,15 @@ class _RealmCore { bool userEquals(User first, User second) => _equals(first.handle, second.handle); bool subscriptionEquals(Subscription first, Subscription second) => _equals(first.handle, second.handle); + int objectGetHashCode(RealmObjectBase value) { + final link = realmCore._getObjectAsLink(value); + + var hashCode = -986587137; + hashCode = (hashCode * -1521134295) + link.classKey; + hashCode = (hashCode * -1521134295) + link.targetKey; + return hashCode; + } + RealmResultsHandle resultsSnapshot(RealmResults results) { final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_snapshot(results.handle._pointer)); return RealmResultsHandle._(resultsPointer, results.realm.handle); diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index d860f2b147..670ad89ec8 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -383,9 +383,21 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab if (identical(this, other)) return true; if (other is! RealmObjectBase) return false; if (!isManaged || !other.isManaged) return false; + return realmCore.objectEquals(this, other); } + late final int _managedHashCode = realmCore.objectGetHashCode(this); + + @override + int get hashCode { + if (!isManaged) { + return super.hashCode; + } + + return _managedHashCode; + } + /// Gets a value indicating whether this object is managed and represents a row in the database. /// /// If a managed object has been removed from the [Realm], it is no longer valid and accessing properties on it diff --git a/test/realm_object_test.dart b/test/realm_object_test.dart index 4e35915659..aea77660fc 100644 --- a/test/realm_object_test.dart +++ b/test/realm_object_test.dart @@ -18,7 +18,6 @@ // ignore_for_file: unused_local_variable, avoid_relative_lib_imports -import 'dart:io'; import 'dart:typed_data'; import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; @@ -723,4 +722,102 @@ Future main([List? args]) async { if (count > 1) fail('Should only receive one event'); } }); + + test('RealmObject read deleted object properties', () { + var config = Configuration.local([Team.schema, Person.schema]); + var realm = getRealm(config); + + var team = Team("TeamOne"); + realm.write(() => realm.add(team)); + var teams = realm.all(); + var teamBeforeDelete = teams[0]; + realm.write(() => realm.delete(team)); + expect(team.isValid, false); + expect(teamBeforeDelete.isValid, false); + expect(team, teamBeforeDelete); + expect(() => team.name, throws("Accessing object of type Team which has been invalidated or deleted")); + expect(() => teamBeforeDelete.name, throws("Accessing object of type Team which has been invalidated or deleted")); + }); + + test('RealmObject.hashCode changes after adding to Realm', () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + final team = Team("TeamOne"); + + final unmanagedHash = team.hashCode; + + realm.write(() => realm.add(team)); + + final managedHash = team.hashCode; + + expect(managedHash, isNot(unmanagedHash)); + expect(managedHash, equals(team.hashCode)); + }); + + test('RealmObject.hashCode is different for different objects', () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + final a = Team("a"); + final b = Team("b"); + + expect(a.hashCode, isNot(b.hashCode)); + + realm.write(() { + realm.add(a); + realm.add(b); + }); + + expect(a.hashCode, isNot(b.hashCode)); + }); + + test('RealmObject.hashCode is same for equal objects', () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + final team = Team("TeamOne"); + + realm.write(() { + realm.add(team); + }); + + final teamAgain = realm.all().first; + + expect(team.hashCode, equals(teamAgain.hashCode)); + }); + + test('RealmObject.hashCode remains stable after deletion', () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + final team = Team("TeamOne"); + + realm.write(() { + realm.add(team); + }); + + final teamAgain = realm.all().first; + + final managedHash = team.hashCode; + + realm.write(() => realm.delete(team)); + + expect(team.hashCode, equals(managedHash)); // Object that was just deleted shouldn't change its hash code + expect(teamAgain.hashCode, equals(managedHash)); // Object that didn't hash its hash code and its row got deleted should still have the same hash code + }); + + test("RealmObject when added to set doesn't have duplicates", () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + realm.write(() { + realm.add(Team("TeamOne")); + }); + + final setOne = realm.all().toSet(); + final setTwo = realm.all().toSet(); + + expect(setOne.difference(setTwo).length, 0); + }); } diff --git a/test/subscription_test.dart b/test/subscription_test.dart index a059d12fec..fbbae0e9ce 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -592,7 +592,7 @@ Future main([List? args]) async { final writeReason = sessionError.compensatingWrites!.first; expect(writeReason, isNotNull); expect(writeReason.objectType, "Product"); - expect(writeReason.reason, 'write to "$productId" in table "${writeReason.objectType}" not allowed; object is outside of the current query view'); + expect(writeReason.reason, 'write to ObjectID("$productId") in table "${writeReason.objectType}" not allowed; object is outside of the current query view'); expect(writeReason.primaryKey.value, productId); }); } From 538c6aa04837d2e70c494125eacb986b3054c628 Mon Sep 17 00:00:00 2001 From: Desislava Stefanova <95419820+desistefanova@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:23:22 +0200 Subject: [PATCH 04/16] Core 13.20.1 sync errors changes (#1387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blagoev Co-authored-by: Kasper Overgård Nielsen Co-authored-by: Kasper Overgård Nielsen Co-authored-by: Nikola Irinchev --- CHANGELOG.md | 21 +- lib/src/app.dart | 2 + lib/src/configuration.dart | 243 ++++++++--------- lib/src/native/realm_bindings.dart | 417 ++++++++++++++++++----------- lib/src/native/realm_core.dart | 52 +--- lib/src/realm_class.dart | 42 +-- lib/src/session.dart | 81 +++++- lib/src/user.dart | 10 +- src/realm-core | 2 +- src/realm_dart_sync.cpp | 29 +- src/realm_dart_sync.h | 2 +- test/app_test.dart | 9 +- test/asymmetric_test.dart | 40 +-- test/asymmetric_test.g.dart | 142 ---------- test/client_reset_test.dart | 105 ++++---- test/configuration_test.dart | 27 +- test/credentials_test.dart | 16 +- test/embedded_test.dart | 18 +- test/list_test.dart | 2 +- test/manual_test.dart | 2 +- test/realm_logger_test.dart | 13 +- test/realm_set_test.dart | 7 +- test/realm_test.dart | 77 +++--- test/session_test.dart | 65 +---- test/subscription_test.dart | 43 +-- test/test.dart | 73 ++++- test/test.g.dart | 135 ++++++++++ test/user_test.dart | 25 -- 28 files changed, 866 insertions(+), 834 deletions(-) delete mode 100644 test/asymmetric_test.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a47287913..cf8a88432b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,35 @@ ### Enhancements * Suppressing rules for a *.g.dart files ([#1413](https://github.com/realm/realm-dart/pull/1413)) +* Full text search supports searching for prefix only. Eg. "description TEXT 'alex*'" (Core upgrade) +* Unknown protocol errors received from the baas server will no longer cause the application to crash if a valid error action is also received. (Core upgrade) +* Added support for server log messages that are enabled by sync protocol version 10. AppServices request id will be provided in a server log message in a future server release. (Core upgrade) +* Simplified sync errors. The following sync errors and error codes are deprecated ([#1387](https://github.com/realm/realm-dart/pull/1387)): + * `SyncClientError`, `SyncConnectionError`, `SyncSessionError`, `SyncWebSocketError`, `GeneralSyncError` - replaced by `SyncError`. + * `SyncClientErrorCode`, `SyncConnectionErrorCode`, `SyncSessionErrorCode`, `SyncWebSocketErrorCode`, `GeneralSyncErrorCode, SyncErrorCategory` - replaced by `SyncErrorCode`. +* Throw an exception if `File::unlock` has failed, in order to inform the SDK that we are likely hitting some limitation on the OS filesystem, instead of crashing the application and use the same file locking logic for all the platforms. (Core upgrade) ### Fixed * Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) +* Crash when querying the size of a Object property through a link chain (Core upgrade, since v13.17.2) +* Deprecated `App.localAppName` and `App.localAppVersion`. They were not used by the server and were not needed to set them. ([#1387](https://github.com/realm/realm-dart/pull/1387)) +* Fixed crash in slab allocator (`Assertion failed: ref + size <= next->first`). (Core upgrade, since 13.0.0) +* Sending empty UPLOAD messages may lead to 'Bad server version' errors and client reset. (Core upgrade, since v11.8.0) +* If a user was logged out while an access token refresh was in progress, the refresh completing would mark the user as logged in again and the user would be in an inconsistent state. (Core 13.21.0) +* Receiving a `write_not_allowed` error from the server would have led to a crash. (Core 13.22.0) +* Fix interprocess locking for concurrent realm file access resulting in a interprocess deadlock on FAT32/exFAT filesystems. (Core 13.23.0) * Fixed RealmObject not overriding `hashCode`, which would lead to sets of RealmObjects potentially containing duplicates. ([#1418](https://github.com/realm/realm-dart/issues/1418)) ### Compatibility * Realm Studio: 13.0.0 or later. ### Internal -* Using Core x.y.z. +* Made binding a `sync::Session` exception safe so if a `MultipleSyncAgents` exception is thrown, the sync client can be torn down safely. (Core upgrade, since 13.4.1) +* Add information about the reason a synchronization session is used for to flexible sync client BIND message. (Core upgrade) +* Sync protocol version bumped to 10. (Core upgrade) +* Handle `badChangeset` error when printing changeset contents in debug. (Core upgrade) + +* Using Core 13.20.1. ## 1.5.0 (2023-09-18) diff --git a/lib/src/app.dart b/lib/src/app.dart index aa49697727..298234efd7 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 cd63401d3f..c632424070 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -22,7 +22,6 @@ import 'dart:io'; // ignore: no_leading_underscores_for_library_prefixes import 'package:path/path.dart' as _path; - import 'native/realm_core.dart'; import 'realm_class.dart'; import 'init.dart'; @@ -603,6 +602,7 @@ class ClientResetError extends SyncError { final App? _app; /// If true the received error is fatal. + @Deprecated("This will be removed in the future.") final bool isFatal = true; /// The path to the original copy of the realm when the client reset was triggered. @@ -612,34 +612,38 @@ class ClientResetError extends SyncError { /// The path where the backup copy of the realm will be placed once the client reset process is complete. final String? backupFilePath; - /// The [ClientResetError] has error code of [SyncClientErrorCode.autoClientResetFailure] - /// when a client reset fails and `onManualResetFallback` occurs. Otherwise, it is [SyncClientErrorCode.unknown] - 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. - SyncSessionErrorCode get sessionErrorCode => SyncSessionErrorCode.fromInt(codeValue); + @Deprecated("This will be removed in the future.") + SyncSessionErrorCode get sessionErrorCode => SyncSessionErrorCode.unknown; @Deprecated("ClientResetError constructor is deprecated and will be removed in the future") ClientResetError( - String message, { - App? app, + String message, + this._app, { SyncErrorCategory category = SyncErrorCategory.client, int? errorCodeValue, this.backupFilePath, this.originalFilePath, String? detailedMessage, - }) : _app = app, - super( + }) : super( message, category, errorCodeValue ?? SyncClientErrorCode.autoClientResetFailure.code, detailedMessage: detailedMessage, ); + ClientResetError._( + String message, + SyncErrorCode code, + this._app, { + this.backupFilePath, + this.originalFilePath, + }) : super._(message, code); + @override String toString() { - return "ClientResetError message: $message category: $category code: $code isFatal: $isFatal"; + return "ClientResetError message: $message"; } /// Initiates the client reset process. @@ -649,70 +653,128 @@ class ClientResetError extends SyncError { if (_app == null) { throw RealmException("This `ClientResetError` does not have an `Application` instance."); } + if (originalFilePath == null) { throw RealmException("Missing `originalFilePath`"); } + return realmCore.immediatelyRunFileActions(_app!, originalFilePath!); } } /// Thrown when an error occurs during synchronization +/// This error or its subclasses will be returned to users through [FlexibleSyncConfiguration.syncErrorHandler] +/// and the exact reason must be found in the `message`. /// {@category Sync} class SyncError extends RealmError { + /// The code that describes this error. + final SyncErrorCode code; + + SyncError._(String message, this.code) : super(message); + /// The numeric code value indicating the type of the sync error. - final int codeValue; + @Deprecated("Errors of SyncError subclasses will be created base on the error code. Error codes won't be returned anymore.") + int get codeValue => code.code; /// The category of the sync error - final SyncErrorCategory category; + @Deprecated("SyncErrorCategory enum is deprecated.") + late SyncErrorCategory category = SyncErrorCategory.system; /// Detailed error message. /// In case of server error, it contains the link to the server log. - final String? detailedMessage; + @Deprecated("Detailed message is empty. Use `message` property.") + late 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); + SyncError(String message, this.category, int codeValue, {this.detailedMessage}) + : code = SyncErrorCode.fromInt(codeValue), + 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); - } + return SyncError._(message, SyncErrorCode.fromInt(code)); } - /// As a specific [SyncError] type. - T as() => this as T; + @override + String toString() { + return "Sync Error: $message"; + } +} + +/// Contains the details for a compensating write performed by the server. +/// {@category Sync} +class CompensatingWriteInfo { + /// The type of the object which was affected by the compensating write. + final String objectType; + + /// The reason for the server to perform a compensating write. + final String reason; + + /// The primary key of the object which was affected by the compensating write. + final RealmValue primaryKey; + + const CompensatingWriteInfo(this.objectType, this.reason, this.primaryKey); + + @override + String toString() { + return "CompensatingWriteInfo: objectType: '$objectType' reason: '$reason' primaryKey: '$primaryKey'"; + } +} + +/// An error type that describes a compensating write error, +/// which indicates that one more object changes have been reverted +/// by the server. +/// {@category Sync} +final class CompensatingWriteError extends SyncError { + /// The list of the compensating writes performed by the server. + late final List? compensatingWrites; + + CompensatingWriteError._( + String message, { + this.compensatingWrites, + }) : super._(message, SyncErrorCode.compensatingWrite); @override String toString() { - return "SyncError message: $message category: $category code: $codeValue"; + return "CompensatingWriteError: $message. ${compensatingWrites ?? ''}"; + } +} + +/// @nodoc +extension SyncErrorInternal on SyncError { + static SyncError createSyncError(SyncErrorDetails error, {App? app}) { + //Client reset can be requested with isClientResetRequested disregarding the ErrorCode + SyncErrorCode errorCode = SyncErrorCode.fromInt(error.code); + + return switch (errorCode) { + SyncErrorCode.autoClientResetFailed => ClientResetError._( + error.message, + errorCode, + app, + originalFilePath: error.originalFilePath, + backupFilePath: error.backupFilePath, + ), + SyncErrorCode.clientReset => + ClientResetError._(error.message, errorCode, app, originalFilePath: error.originalFilePath, backupFilePath: error.backupFilePath), + SyncErrorCode.compensatingWrite => CompensatingWriteError._( + error.message, + compensatingWrites: error.compensatingWrites, + ), + _ => SyncError._(error.message, errorCode), + }; } } +// Deprecated errors - to be removed in 2.0 + /// 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; - /// The [SyncClientErrorCode] value indicating the type of the sync error. - SyncClientErrorCode get code => SyncClientErrorCode.fromInt(codeValue); - @Deprecated("SyncClientError constructor is deprecated and will be removed in the future") SyncClientError( String message, @@ -730,13 +792,11 @@ 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; - /// The [SyncConnectionErrorCode] value indicating the type of the sync error. - SyncConnectionErrorCode get code => SyncConnectionErrorCode.fromInt(codeValue); - @Deprecated("SyncConnectionError constructor is deprecated and will be removed in the future") SyncConnectionError( String message, @@ -754,13 +814,11 @@ 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; - /// The [SyncSessionErrorCode] value indicating the type of the sync error. - SyncSessionErrorCode get code => SyncSessionErrorCode.fromInt(codeValue); - @Deprecated("SyncSessionError constructor is deprecated and will be removed in the future") SyncSessionError( String message, @@ -780,11 +838,8 @@ class SyncSessionError extends SyncError { /// /// 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") +@Deprecated("Use SyncError.") 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, @@ -798,10 +853,8 @@ class SyncResolveError extends SyncError { } /// Web socket error +@Deprecated("Use SyncError.") 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( String message, @@ -817,10 +870,8 @@ 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( String message, @@ -836,6 +887,7 @@ class GeneralSyncError extends SyncError { } /// General sync error codes +@Deprecated("Use SyncError.") enum GeneralSyncErrorCode { /// Unknown Sync error code unknown(9999); @@ -849,84 +901,3 @@ enum GeneralSyncErrorCode { final int code; const GeneralSyncErrorCode(this.code); } - -/// Contains the details for a compensating write performed by the server. -/// {@category Sync} -class CompensatingWriteInfo { - /// The type of the object which was affected by the compensating write. - final String objectType; - - /// The reason for the server to perform a compensating write. - final String reason; - - /// The primary key of the object which was affected by the compensating write. - final RealmValue primaryKey; - - const CompensatingWriteInfo(this.objectType, this.reason, this.primaryKey); - - @override - String toString() { - return "CompensatingWriteInfo: objectType: '$objectType' reason: '$reason' primaryKey: '$primaryKey'"; - } -} - -/// An error type that describes a compensating write error, -/// which indicates that one more object changes have been reverted -/// by the server. -/// {@category Sync} -class CompensatingWriteError extends SyncError { - /// The [CompensatingWriteError] has error code of [SyncSessionErrorCode.compensatingWrite] - SyncSessionErrorCode get code => SyncSessionErrorCode.compensatingWrite; - - /// The list of the compensating writes performed by the server. - late final List? compensatingWrites; - - CompensatingWriteError._( - String message, { - String? detailedMessage, - this.compensatingWrites, - }) : super(message, SyncErrorCategory.session, SyncSessionErrorCode.compensatingWrite.code, detailedMessage: detailedMessage); - - @override - String toString() { - return "CompensatingWriteError message: $message category: $category code: $code. ${compensatingWrites ?? ''}"; - } -} - -/// @nodoc -extension SyncErrorInternal on SyncError { - static SyncError createSyncError(SyncErrorDetails error, {App? app}) { - 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); - } - } -} diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index d0a0a61ad7..8a5ef8b252 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. @@ -9901,29 +9858,24 @@ class RealmLibrary { ffi.Pointer, realm_free_userdata_func_t)>(); - void realm_sync_socket_callback_complete( - ffi.Pointer realm_callback, - int status, - ffi.Pointer reason, - ) { - return _realm_sync_socket_callback_complete( - realm_callback, - status, - reason, - ); - } - - late final _realm_sync_socket_callback_completePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Int32, - ffi.Pointer)>>('realm_sync_socket_callback_complete'); - late final _realm_sync_socket_callback_complete = - _realm_sync_socket_callback_completePtr.asFunction< - void Function(ffi.Pointer, int, - ffi.Pointer)>(); - + /// Creates a new sync socket instance for the Sync Client that handles the operations for a custom + /// websocket and event loop implementation. + /// @param userdata CAPI implementation specific pointer containing custom context data that is provided to + /// each of the provided functions. + /// @param userdata_free function that will be called when the sync socket is destroyed to delete userdata. This + /// is required if userdata is not null. + /// @param post_func function that will be called to post a callback handler onto the event loop - use the + /// realm_sync_socket_post_complete() function when the callback handler is scheduled to run. + /// @param create_timer_func function that will be called to create a new timer resource with the callback + /// handler that will be run when the timer expires or an erorr occurs - use the + /// realm_sync_socket_timer_canceled() function if the timer is canceled or the + /// realm_sync_socket_timer_complete() function if the timer expires or an error occurs. + /// @param cancel_timer_func function that will be called when the timer has been canceled by the sync client. + /// @param free_timer_func function that will be called when the timer resource has been destroyed by the sync client. + /// @param websocket_connect_func function that will be called when the sync client creates a websocket. + /// @param websocket_write_func function that will be called when the sync client sends data over the websocket. + /// @param websocket_free_func function that will be called when the sync client closes the websocket conneciton. + /// @return a realm_sync_socket_t pointer suitable for passing to realm_sync_client_config_set_sync_socket() ffi.Pointer realm_sync_socket_new( ffi.Pointer userdata, realm_free_userdata_func_t userdata_free, @@ -9973,32 +9925,132 @@ class RealmLibrary { realm_sync_socket_websocket_async_write_func_t, realm_sync_socket_websocket_free_func_t)>(); - void realm_sync_socket_websocket_closed( + /// To be called to execute the callback function provided to the post_func when the event loop executes + /// that post'ed operation. The post_handler resource will automatically be destroyed during this + /// operation. + /// @param post_handler the post callback handler that was originally provided to the post_func + /// @param result the error code for the error that occurred or RLM_ERR_SYNC_SOCKET_SUCCESS if the + /// callback handler should be executed normally. + /// @param reason a string describing details about the error that occurred or empty string if no error. + /// NOTE: This function must be called by the event loop execution thread. + void realm_sync_socket_post_complete( + ffi.Pointer post_handler, + int result, + ffi.Pointer reason, + ) { + return _realm_sync_socket_post_complete( + post_handler, + result, + reason, + ); + } + + late final _realm_sync_socket_post_completePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Int32, + ffi.Pointer)>>('realm_sync_socket_post_complete'); + late final _realm_sync_socket_post_complete = + _realm_sync_socket_post_completePtr.asFunction< + void Function(ffi.Pointer, int, + ffi.Pointer)>(); + + /// To be called to execute the callback handler provided to the create_timer_func when the timer has been + /// canceled. + /// @param timer_handler the timer callback handler that was provided when the timer was created. + /// NOTE: This function must be called by the event loop execution thread. + void realm_sync_socket_timer_canceled( + ffi.Pointer timer_handler, + ) { + return _realm_sync_socket_timer_canceled( + timer_handler, + ); + } + + late final _realm_sync_socket_timer_canceledPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer)>>( + 'realm_sync_socket_timer_canceled'); + late final _realm_sync_socket_timer_canceled = + _realm_sync_socket_timer_canceledPtr.asFunction< + void Function(ffi.Pointer)>(); + + /// To be called to execute the callback handler provided to the create_timer_func when the timer is + /// complete or an error occurs while processing the timer. + /// @param timer_handler the timer callback handler that was provided when the timer was created. + /// @param result the error code for the error that occurred or RLM_ERR_SYNC_SOCKET_SUCCESS if the timer + /// expired normally. + /// @param reason a string describing details about the error that occurred or empty string if no error. + /// NOTE: This function must be called by the event loop execution thread. + void realm_sync_socket_timer_complete( + ffi.Pointer timer_handler, + int result, + ffi.Pointer reason, + ) { + return _realm_sync_socket_timer_complete( + timer_handler, + result, + reason, + ); + } + + late final _realm_sync_socket_timer_completePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Int32, + ffi.Pointer)>>('realm_sync_socket_timer_complete'); + late final _realm_sync_socket_timer_complete = + _realm_sync_socket_timer_completePtr.asFunction< + void Function(ffi.Pointer, int, + ffi.Pointer)>(); + + /// To be called when the websocket has been closed, either due to an error or a normal close operation. + /// @param realm_websocket_observer the websocket observer object that was provided to the websocket_connect_func + /// @param was_clean boolean value that indicates whether this is a normal close situation (true), the + /// close code was provided by the server via a close message (true), or if the close code was + /// generated by the local websocket as a result of some other error (false) (e.g. host + /// unreachable, etc.) + /// @param code the websocket close code (per the WebSocket spec) that describes why the websocket was closed. + /// @param reason a string describing details about the error that occurred or empty string if no error. + /// @return bool designates whether the WebSocket object has been destroyed during the execution of this + /// function. The normal return value is True to indicate the WebSocket object is no longer valid. If + /// False is returned, the WebSocket object will be destroyed at some point in the future. + /// NOTE: This function must be called by the event loop execution thread and should not be called + /// after the websocket_free_func has been called to release the websocket resources. + bool realm_sync_socket_websocket_closed( ffi.Pointer realm_websocket_observer, bool was_clean, - int status, + int code, ffi.Pointer reason, ) { return _realm_sync_socket_websocket_closed( realm_websocket_observer, was_clean, - status, + code, reason, ); } late final _realm_sync_socket_websocket_closedPtr = _lookup< ffi.NativeFunction< - ffi.Void Function( + ffi.Bool Function( ffi.Pointer, ffi.Bool, ffi.Int32, ffi.Pointer)>>('realm_sync_socket_websocket_closed'); late final _realm_sync_socket_websocket_closed = _realm_sync_socket_websocket_closedPtr.asFunction< - void Function(ffi.Pointer, bool, int, + bool Function(ffi.Pointer, bool, int, ffi.Pointer)>(); + /// To be called when the websocket successfully connects to the server. + /// @param realm_websocket_observer the websocket observer object that was provided to the websocket_connect_func + /// @param protocol the value of the Sec-WebSocket-Protocol header in the connect response from the server. + /// NOTE: This function must be called by the event loop execution thread and should not be called + /// after the websocket_free_func has been called to release the websocket resources. void realm_sync_socket_websocket_connected( ffi.Pointer realm_websocket_observer, ffi.Pointer protocol, @@ -10018,6 +10070,12 @@ class RealmLibrary { void Function(ffi.Pointer, ffi.Pointer)>(); + /// To be called when an error occurs - the actual error value will be provided when the websocket_closed + /// function is called. This function informs that the socket object is in an error state and no further + /// TX operations should be performed. + /// @param realm_websocket_observer the websocket observer object that was provided to the websocket_connect_func + /// NOTE: This function must be called by the event loop execution thread and should not be called + /// after the websocket_free_func has been called to release the websocket resources. void realm_sync_socket_websocket_error( ffi.Pointer realm_websocket_observer, ) { @@ -10034,7 +10092,17 @@ class RealmLibrary { _realm_sync_socket_websocket_errorPtr .asFunction)>(); - void realm_sync_socket_websocket_message( + /// To be called to provide the received data to the Sync Client when a write operation has completed. + /// The data buffer can be safely discarded after this function has completed. + /// @param realm_websocket_observer the websocket observer object that was provided to the websocket_connect_func + /// @param data a pointer to the buffer that contains the data received over the websocket + /// @param data_size the number of bytes in the data buffer + /// @return bool designates whether the WebSocket object should continue processing messages. The normal return + /// value is true. False must be returned if the websocket object has been destroyed during execution of + /// the function. + /// NOTE: This function must be called by the event loop execution thread and should not be called + /// after the websocket_free_func has been called to release the websocket resources. + bool realm_sync_socket_websocket_message( ffi.Pointer realm_websocket_observer, ffi.Pointer data, int data_size, @@ -10048,15 +10116,46 @@ class RealmLibrary { late final _realm_sync_socket_websocket_messagePtr = _lookup< ffi.NativeFunction< - ffi.Void Function( + ffi.Bool Function( ffi.Pointer, ffi.Pointer, ffi.Size)>>('realm_sync_socket_websocket_message'); late final _realm_sync_socket_websocket_message = _realm_sync_socket_websocket_messagePtr.asFunction< - void Function(ffi.Pointer, + bool Function(ffi.Pointer, ffi.Pointer, int)>(); + /// To be called to execute the callback function provided to the websocket_write_func when the write + /// operation is complete. The write_handler resource will automatically be destroyed during this + /// operation. + /// @param write_handler the write callback handler that was originally provided to the websocket_write_func + /// @param result the error code for the error that occurred or RLM_ERR_SYNC_SOCKET_SUCCESS if write completed + /// successfully + /// @param reason a string describing details about the error that occurred or empty string if no error. + /// NOTE: This function must be called by the event loop execution thread. + void realm_sync_socket_write_complete( + ffi.Pointer write_handler, + int result, + ffi.Pointer reason, + ) { + return _realm_sync_socket_write_complete( + write_handler, + result, + reason, + ); + } + + late final _realm_sync_socket_write_completePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Int32, + ffi.Pointer)>>('realm_sync_socket_write_complete'); + late final _realm_sync_socket_write_complete = + _realm_sync_socket_write_completePtr.asFunction< + void Function(ffi.Pointer, int, + ffi.Pointer)>(); + /// Access the subscription at index. /// @return the subscription or nullptr if the index is not valid ffi.Pointer realm_sync_subscription_at( @@ -10220,6 +10319,32 @@ class RealmLibrary { ffi.Pointer Function( ffi.Pointer)>(); + /// Remove all subscriptions for a given class type. If operation completes successfully set the bool out param. + /// @return true if no error occurred, false otherwise (use realm_get_last_error for fetching the error). + bool realm_sync_subscription_set_erase_by_class_name( + ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer erased, + ) { + return _realm_sync_subscription_set_erase_by_class_name( + arg0, + arg1, + erased, + ); + } + + late final _realm_sync_subscription_set_erase_by_class_namePtr = _lookup< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>( + 'realm_sync_subscription_set_erase_by_class_name'); + late final _realm_sync_subscription_set_erase_by_class_name = + _realm_sync_subscription_set_erase_by_class_namePtr.asFunction< + bool Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + /// Erase from subscription set by id. If operation completes successfully set the bool out param. /// @return true if no error occurred, false otherwise (use realm_get_last_error for fetching the error). bool realm_sync_subscription_set_erase_by_id( @@ -10671,20 +10796,6 @@ class RealmLibrary { late final _realm_user_get_app = _realm_user_get_appPtr.asFunction< ffi.Pointer Function(ffi.Pointer)>(); - int realm_user_get_auth_provider( - ffi.Pointer arg0, - ) { - return _realm_user_get_auth_provider( - arg0, - ); - } - - late final _realm_user_get_auth_providerPtr = _lookup< - ffi.NativeFunction)>>( - 'realm_user_get_auth_provider'); - late final _realm_user_get_auth_provider = _realm_user_get_auth_providerPtr - .asFunction)>(); - /// Get the custom user data from the user's access token. /// /// Returned value must be manually released with realm_free(). @@ -10739,21 +10850,6 @@ class RealmLibrary { late final _realm_user_get_identity = _realm_user_get_identityPtr .asFunction Function(ffi.Pointer)>(); - ffi.Pointer realm_user_get_local_identity( - ffi.Pointer arg0, - ) { - return _realm_user_get_local_identity( - arg0, - ); - } - - late final _realm_user_get_local_identityPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('realm_user_get_local_identity'); - late final _realm_user_get_local_identity = _realm_user_get_local_identityPtr - .asFunction Function(ffi.Pointer)>(); - /// Get the user profile associated with this user. /// /// Returned value must be manually released with realm_free(). @@ -11028,7 +11124,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 +11425,23 @@ 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_CONNECT_TIMEOUT = 1035; + static const int RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE = 1036; + static const int RLM_ERR_SYNC_PERMISSION_DENIED = 1037; + static const int RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED = 1038; + static const int RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED = 1039; + static const int RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED = 1040; + static const int RLM_ERR_SYNC_USER_MISMATCH = 1041; + static const int RLM_ERR_TLS_HANDSHAKE_FAILED = 1042; + static const int RLM_ERR_WRONG_SYNC_TYPE = 1043; + static const int RLM_ERR_SYNC_WRITE_NOT_ALLOWED = 1044; 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 +11513,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 +11555,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 +12054,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 +12097,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; @@ -12089,24 +12169,36 @@ final class realm_sync_socket extends ffi.Opaque {} final class realm_sync_socket_callback extends ffi.Opaque {} -typedef realm_sync_socket_callback_t = realm_sync_socket_callback; +abstract class realm_sync_socket_callback_result { + static const int RLM_ERR_SYNC_SOCKET_SUCCESS = 0; + static const int RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED = 1027; + static const int RLM_ERR_SYNC_SOCKET_RUNTIME = 1000; + static const int RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY = 1003; + static const int RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED = 1005; + static const int RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED = 1030; + static const int RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED = 2001; + static const int RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT = 3000; +} + typedef realm_sync_socket_connect_func_t = ffi.Pointer< ffi.NativeFunction< realm_sync_socket_websocket_t Function( ffi.Pointer userdata, realm_websocket_endpoint_t endpoint, - ffi.Pointer realm_websocket_observer)>>; + ffi.Pointer websocket_observer)>>; typedef realm_sync_socket_create_timer_func_t = ffi.Pointer< ffi.NativeFunction< realm_sync_socket_timer_t Function( ffi.Pointer userdata, ffi.Uint64 delay_ms, - ffi.Pointer realm_callback)>>; + ffi.Pointer timer_callback)>>; +typedef realm_sync_socket_post_callback_t = realm_sync_socket_callback; typedef realm_sync_socket_post_func_t = ffi.Pointer< ffi.NativeFunction< ffi.Void Function(ffi.Pointer userdata, - ffi.Pointer realm_callback)>>; + ffi.Pointer post_callback)>>; typedef realm_sync_socket_t = realm_sync_socket; +typedef realm_sync_socket_timer_callback_t = realm_sync_socket_callback; typedef realm_sync_socket_timer_canceled_func_t = ffi.Pointer< ffi.NativeFunction< ffi.Void Function(ffi.Pointer userdata, @@ -12120,15 +12212,16 @@ typedef realm_sync_socket_websocket_async_write_func_t = ffi.Pointer< ffi.NativeFunction< ffi.Void Function( ffi.Pointer userdata, - realm_sync_socket_websocket_t websocket_userdata, + realm_sync_socket_websocket_t websocket, ffi.Pointer data, ffi.Size size, - ffi.Pointer realm_callback)>>; + ffi.Pointer write_callback)>>; typedef realm_sync_socket_websocket_free_func_t = ffi.Pointer< ffi.NativeFunction< ffi.Void Function(ffi.Pointer userdata, - realm_sync_socket_websocket_t websocket_userdata)>>; + realm_sync_socket_websocket_t websocket)>>; typedef realm_sync_socket_websocket_t = ffi.Pointer; +typedef realm_sync_socket_write_callback_t = realm_sync_socket_callback; typedef realm_sync_ssl_verify_func_t = ffi.Pointer< ffi.NativeFunction< ffi.Bool Function( @@ -12149,8 +12242,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 df0e985820..7f97f8fd17 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,8 @@ 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 success = _realmLib.realm_get_async_error(error, err); + completer.completeError(RealmException("Failed to open realm ${success ? err.ref.toLastError().toString() : ''}")); return; } @@ -1638,18 +1638,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; }); } @@ -2228,11 +2216,6 @@ class _RealmCore { return deviceId.cast().toRealmDartString(treatEmptyAsNull: true, freeRealmMemory: true); } - AuthProviderType userGetAuthProviderType(User user) { - final provider = _realmLib.realm_user_get_auth_provider(user.handle._pointer); - return AuthProviderTypeInternal.getByValue(provider); - } - AuthProviderType userGetCredentialsProviderType(Credentials credentials) { final provider = _realmLib.realm_auth_credentials_get_provider(credentials.handle._pointer); return AuthProviderTypeInternal.getByValue(provider); @@ -2339,7 +2322,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); @@ -2349,7 +2332,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); @@ -2357,7 +2340,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; @@ -3195,19 +3178,14 @@ 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.error, isFatal: is_fatal, isClientResetRequested: is_client_reset_requested, originalFilePath: userInfoMap?[originalFilePathKey], @@ -3250,10 +3228,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.error); return SyncErrorInternal.createSyncError(details); } } @@ -3411,19 +3389,17 @@ extension PlatformEx on Platform { /// @nodoc class SyncErrorDetails { final String message; - final SyncErrorCategory category; 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.code, { - this.detailedMessage, + this.path, this.isFatal = false, this.isClientResetRequested = false, this.originalFilePath, diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 38044f8fb5..da4b5c508d 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -15,6 +15,7 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// + import 'dart:async'; import 'dart:ffi'; import 'dart:io'; @@ -71,8 +72,6 @@ export "configuration.dart" DiscardUnsyncedChangesHandler, DisconnectedSyncConfiguration, FlexibleSyncConfiguration, - GeneralSyncError, - GeneralSyncErrorCode, InitialDataCallback, InMemoryConfiguration, LocalConfiguration, @@ -83,12 +82,21 @@ export "configuration.dart" RecoverUnsyncedChangesHandler, SchemaObject, ShouldCompactCallback, - SyncClientError, - SyncConnectionError, SyncError, SyncErrorHandler, + // ignore: deprecated_member_use_from_same_package + SyncClientError, + // ignore: deprecated_member_use_from_same_package + SyncConnectionError, + // ignore: deprecated_member_use_from_same_package + GeneralSyncError, + // ignore: deprecated_member_use_from_same_package + GeneralSyncErrorCode, + // ignore: deprecated_member_use_from_same_package SyncResolveError, + // ignore: deprecated_member_use_from_same_package SyncWebSocketError, + // ignore: deprecated_member_use_from_same_package SyncSessionError; export 'credentials.dart' show AuthProviderType, Credentials, EmailPasswordAuthProvider; export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges, ListExtension; @@ -117,11 +125,18 @@ export 'session.dart' ConnectionState, Session, SessionState, + SyncErrorCode, + // ignore: deprecated_member_use_from_same_package SyncClientErrorCode, + // ignore: deprecated_member_use_from_same_package SyncConnectionErrorCode, + // ignore: deprecated_member_use_from_same_package SyncErrorCategory, + // ignore: deprecated_member_use_from_same_package SyncResolveErrorCode, + // ignore: deprecated_member_use_from_same_package SyncWebSocketErrorCode, + // ignore: deprecated_member_use_from_same_package SyncSessionErrorCode; export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet; export 'user.dart' show User, UserState, ApiKeyClient, UserIdentity, ApiKey, FunctionsClient; @@ -571,26 +586,15 @@ class Realm implements Finalizable { throw RealmException("Can't compact an in-memory Realm"); } - late Configuration compactConfig; - if (!File(config.path).existsSync()) { + print("realm file doesn't exist: ${config.path}"); return false; } - if (config is LocalConfiguration) { - // `compact` opens the realm file so it can triger schema version upgrade, file format upgrade, migration and initial data callbacks etc. - // We must allow that to happen so use the local config as is. - compactConfig = config; - } else if (config is DisconnectedSyncConfiguration) { - compactConfig = config; - } else if (config is FlexibleSyncConfiguration) { - compactConfig = Configuration.disconnectedSync(config.schemaObjects.toList(), - path: config.path, fifoFilesFallbackPath: config.fifoFilesFallbackPath, encryptionKey: config.encryptionKey); - } else { - throw RealmError("Unsupported realm configuration type ${config.runtimeType}"); + final realm = Realm(config); + if (config is FlexibleSyncConfiguration) { + realm.syncSession.pause(); } - - final realm = Realm(compactConfig); try { return realmCore.compact(realm); } finally { diff --git a/lib/src/session.dart b/lib/src/session.dart index 26ca4b8f65..91c8fef07c 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -22,6 +22,8 @@ import '../realm.dart'; import 'native/realm_core.dart'; import 'user.dart'; +import '../src/native/realm_bindings.dart'; + /// An object encapsulating a synchronization session. Sessions represent the /// communication between the client (and a local Realm file on disk), and the /// server. Sessions are always created by the SDK and vended out through various @@ -74,10 +76,6 @@ class Session implements Finalizable { final controller = SessionConnectionStateController(this); return controller.createStream(); } - - void _raiseSessionError(SyncErrorCategory category, int errorCode, bool isFatal) { - realmCore.raiseError(this, category, errorCode, isFatal); - } } /// A type containing information about the progress state at a given instant. @@ -121,8 +119,8 @@ extension SessionInternal on Session { return _handle; } - void raiseError(SyncErrorCategory category, int errorCode, bool isFatal) { - realmCore.raiseError(this, category, errorCode, isFatal); + void raiseError(int errorCode, bool isFatal) { + realmCore.raiseError(this, errorCode, isFatal); } static SyncProgress createSyncProgress(int transferredBytes, int transferableBytes) => @@ -245,7 +243,74 @@ enum ProgressMode { forCurrentlyOutstandingWork, } +/// Error code enumeration, indicating the type of [SyncError]. +enum SyncErrorCode { + /// Unrecognized error code. It usually indicates incompatibility between the App Services server and client SDK versions. + runtimeError(realm_errno.RLM_ERR_RUNTIME), + + /// The partition value specified by the user is not valid - i.e. its the wrong type or is encoded incorrectly. + badPartitionValue(realm_errno.RLM_ERR_BAD_SYNC_PARTITION_VALUE), + + /// A fundamental invariant in the communication between the client and the server was not upheld. This typically indicates + /// a bug in the synchronization layer and should be reported at https://github.com/realm/realm-core/issues. + protocolInvariantFailed(realm_errno.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), + + /// The changeset is invalid. + badChangeset(realm_errno.RLM_ERR_BAD_CHANGESET), + + /// The client attempted to create a subscription for a query is invalid/malformed. + invalidSubscriptionQuery(realm_errno.RLM_ERR_INVALID_SUBSCRIPTION_QUERY), + + /// A client reset has occurred. This error code will only be reported via a [ClientResetError] and only + /// in the case manual client reset handling is required - either via [ManualRecoveryHandler] or when + /// `onManualReset` is invoked on one of the automatic client reset handlers. + clientReset(realm_errno.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), + + /// The client attempted to upload an invalid schema change - either an additive schema change + /// when developer mode is off or a destructive schema change. + invalidSchemaChange(realm_errno.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), + + /// Permission to Realm has been denied. + permissionDenied(realm_errno.RLM_ERR_SYNC_PERMISSION_DENIED), + + /// The server permissions for this file have changed since the last time it was used. + serverPermissionsChanged(realm_errno.RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED), + + /// The user for this session doesn't match the user who originally created the file. This can happen + /// if you explicitly specify the Realm file path in the configuration and you open the Realm first with + /// user A, then with user B without changing the on-disk path. + userMismatch(realm_errno.RLM_ERR_SYNC_USER_MISMATCH), + + /// Client attempted a write that is disallowed by permissions, or modifies an object + /// outside the current query - this will result in a [CompensatingWriteError]. + writeNotAllowed(realm_errno.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), + + /// Automatic client reset has failed. This will only be reported via [ClientResetError] + /// when an automatic client reset handler was used but it failed to perform the client reset operation - + /// typically due to a breaking schema change in the server schema or due to an exception occurring in the + /// before or after client reset callbacks. + autoClientResetFailed(realm_errno.RLM_ERR_AUTO_CLIENT_RESET_FAILED), + + /// The wrong sync type was used to connect to the server. This means that you're trying to connect + /// to an app configured to use partition sync. + wrongSyncType(realm_errno.RLM_ERR_WRONG_SYNC_TYPE), + + /// Client attempted a write that is disallowed by permissions, or modifies an + /// object outside the current query, and the server undid the modification. + compensatingWrite(realm_errno.RLM_ERR_SYNC_COMPENSATING_WRITE); + + static final Map _valuesMap = {for (var value in SyncErrorCode.values) value.code: value}; + + static SyncErrorCode fromInt(int code) { + return SyncErrorCode._valuesMap[code] ?? SyncErrorCode.runtimeError; + } + + final int code; + const SyncErrorCode(this.code); +} + /// The category of a [SyncError]. +@Deprecated("Sync errors are not classified by SyncErrorCategory anymore.") enum SyncErrorCategory { /// The error originated from the client client, @@ -271,6 +336,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 SyncError or its subclasses instead.") enum SyncClientErrorCode { /// Connection closed (no error) connectionClosed(100), @@ -382,6 +448,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 SyncError or its subclasses instead of using error codes.") enum SyncConnectionErrorCode { // Connection level and protocol errors /// Connection closed (no error) @@ -445,6 +512,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 SyncError or its subclasses instead of using error codes.") enum SyncSessionErrorCode { /// Session closed (no error) sessionClosed(200), @@ -598,6 +666,7 @@ enum SyncResolveErrorCode { /// Web socket errors. /// /// These errors will be reported via the error handlers of the affected sessions. +@Deprecated("Use SyncError or its subclasses instead of using error codes.") enum SyncWebSocketErrorCode { /// Web socket resolution failed websocketResolveFailed(4400), diff --git a/lib/src/user.dart b/lib/src/user.dart index 1ca8c08196..778434e715 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -47,7 +47,6 @@ class User { /// [API Keys Authentication Docs](https://docs.mongodb.com/realm/authentication/api-key/) ApiKeyClient get apiKeys { _ensureLoggedIn('access API keys'); - _ensureCanAccessAPIKeys(); return _apiKeys; } @@ -89,8 +88,9 @@ class User { } /// Gets the [AuthProviderType] this [User] is currently logged in with. + @Deprecated("Get the auth provider from the user identity.") AuthProviderType get provider { - return realmCore.userGetAuthProviderType(this); + return identities.first.provider; } /// Gets the profile information for this [User]. @@ -161,12 +161,6 @@ class User { throw RealmError('User must be logged in to $clarification'); } } - - void _ensureCanAccessAPIKeys() { - if (provider == AuthProviderType.apiKey) { - throw RealmError('Users logged in with API key cannot manage API keys'); - } - } } /// The current state of a [User]. diff --git a/src/realm-core b/src/realm-core index 21925e4d81..56a048d9f4 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit 21925e4d81598959bd128ad0e687ce95efd12c45 +Subproject commit 56a048d9f450445f2d52c7405f3019a533bdaa3f diff --git a/src/realm_dart_sync.cpp b/src/realm_dart_sync.cpp index 8846e3aae3..ba148f972f 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,10 @@ 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; + // TODO: Map usercode_error and path when issue https://github.com/realm/realm-core/issues/6925 is fixed 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 +115,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,15 +128,16 @@ 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) { + error = error_input.error; + categories = error_input.categories; message = message_buffer.c_str(); } @@ -189,10 +194,10 @@ bool invoke_dart_and_await_result(realm::util::UniqueFunction 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), @@ -262,10 +258,7 @@ Future main([List? args]) async { baasTest('App.reconnect', (appConfiguration) async { final app = App(appConfiguration); - - final user = await app.logIn(Credentials.anonymous()); - final configuration = Configuration.flexibleSync(user, [Task.schema]); - final realm = getRealm(configuration); + final realm = await getIntegrationRealm(app: app); final session = realm.syncSession; // TODO: We miss a way to force a disconnect. Once we implement GenericNetworkTransport diff --git a/test/asymmetric_test.dart b/test/asymmetric_test.dart index c585a9bd5e..a7584b9442 100644 --- a/test/asymmetric_test.dart +++ b/test/asymmetric_test.dart @@ -21,43 +21,11 @@ import 'package:test/expect.dart' hide throws; import '../lib/realm.dart'; import 'test.dart'; -part 'asymmetric_test.g.dart'; - -@RealmModel(ObjectType.asymmetricObject) -class _Asymmetric { - @PrimaryKey() - @MapTo('_id') - late ObjectId id; - - late List<_Embedded> embeddedObjects; -} - -@RealmModel(ObjectType.embeddedObject) -class _Embedded { - late int value; - late RealmValue any; - _Symmetric? symmetric; -} - -@RealmModel() -class _Symmetric { - @PrimaryKey() - @MapTo('_id') - late ObjectId id; -} - Future main([List? args]) async { await setupTests(args); - Future getSyncRealm(AppConfiguration config) async { - final app = App(config); - final user = await getAnonymousUser(app); - final realmConfig = Configuration.flexibleSync(user, [Asymmetric.schema, Embedded.schema, Symmetric.schema]); - return getRealm(realmConfig); - } - baasTest('Asymmetric objects die even before upload', (config) async { - final realm = await getSyncRealm(config); + final realm = await getIntegrationRealm(appConfig: config); realm.syncSession.pause(); final oid = ObjectId(); @@ -74,7 +42,7 @@ Future main([List? args]) async { }); baasTest('Asymmetric re-add same PK', (config) async { - final realm = await getSyncRealm(config); + final realm = await getIntegrationRealm(appConfig: config); final oid = ObjectId(); realm.write(() { @@ -92,7 +60,7 @@ Future main([List? args]) async { }); baasTest('Asymmetric tricks to add non-embedded links', (config) async { - final realm = await getSyncRealm(config); + final realm = await getIntegrationRealm(appConfig: config); realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.add(realm.all()); @@ -115,7 +83,7 @@ Future main([List? args]) async { }); test("Asymmetric don't work with disconnectedSync", () { - final config = Configuration.disconnectedSync([Asymmetric.schema, Embedded.schema, Symmetric.schema], path: 'asymmetric_disconnected.realm'); + final config = Configuration.disconnectedSync([Asymmetric.schema, Embedded.schema, Symmetric.schema], path: generateRandomRealmPath()); expect(() => Realm(config), throws()); }); diff --git a/test/asymmetric_test.g.dart b/test/asymmetric_test.g.dart deleted file mode 100644 index 41344b439a..0000000000 --- a/test/asymmetric_test.g.dart +++ /dev/null @@ -1,142 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asymmetric_test.dart'; - -// ************************************************************************** -// RealmObjectGenerator -// ************************************************************************** - -// ignore_for_file: type=lint -class Asymmetric extends _Asymmetric - with RealmEntity, RealmObjectBase, AsymmetricObject { - Asymmetric( - ObjectId id, { - Iterable embeddedObjects = const [], - }) { - RealmObjectBase.set(this, '_id', id); - RealmObjectBase.set>( - this, 'embeddedObjects', RealmList(embeddedObjects)); - } - - Asymmetric._(); - - @override - ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; - @override - set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); - - @override - RealmList get embeddedObjects => - RealmObjectBase.get(this, 'embeddedObjects') - as RealmList; - @override - set embeddedObjects(covariant RealmList value) => - throw RealmUnsupportedSetError(); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - Asymmetric freeze() => RealmObjectBase.freezeObject(this); - - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { - RealmObjectBase.registerFactory(Asymmetric._); - return const SchemaObject( - ObjectType.asymmetricObject, Asymmetric, 'Asymmetric', [ - SchemaProperty('id', RealmPropertyType.objectid, - mapTo: '_id', primaryKey: true), - SchemaProperty('embeddedObjects', RealmPropertyType.object, - linkTarget: 'Embedded', collectionType: RealmCollectionType.list), - ]); - } -} - -// ignore_for_file: type=lint -class Embedded extends _Embedded - with RealmEntity, RealmObjectBase, EmbeddedObject { - Embedded( - int value, { - RealmValue any = const RealmValue.nullValue(), - Symmetric? symmetric, - }) { - RealmObjectBase.set(this, 'value', value); - RealmObjectBase.set(this, 'any', any); - RealmObjectBase.set(this, 'symmetric', symmetric); - } - - Embedded._(); - - @override - int get value => RealmObjectBase.get(this, 'value') as int; - @override - set value(int value) => RealmObjectBase.set(this, 'value', value); - - @override - RealmValue get any => - RealmObjectBase.get(this, 'any') as RealmValue; - @override - set any(RealmValue value) => RealmObjectBase.set(this, 'any', value); - - @override - Symmetric? get symmetric => - RealmObjectBase.get(this, 'symmetric') as Symmetric?; - @override - set symmetric(covariant Symmetric? value) => - RealmObjectBase.set(this, 'symmetric', value); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - Embedded freeze() => RealmObjectBase.freezeObject(this); - - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { - RealmObjectBase.registerFactory(Embedded._); - return const SchemaObject(ObjectType.embeddedObject, Embedded, 'Embedded', [ - SchemaProperty('value', RealmPropertyType.int), - SchemaProperty('any', RealmPropertyType.mixed, optional: true), - SchemaProperty('symmetric', RealmPropertyType.object, - optional: true, linkTarget: 'Symmetric'), - ]); - } -} - -// ignore_for_file: type=lint -class Symmetric extends _Symmetric - with RealmEntity, RealmObjectBase, RealmObject { - Symmetric( - ObjectId id, - ) { - RealmObjectBase.set(this, '_id', id); - } - - Symmetric._(); - - @override - ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; - @override - set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - Symmetric freeze() => RealmObjectBase.freezeObject(this); - - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { - RealmObjectBase.registerFactory(Symmetric._); - return const SchemaObject(ObjectType.realmObject, Symmetric, 'Symmetric', [ - SchemaProperty('id', RealmPropertyType.objectid, - mapTo: '_id', primaryKey: true), - ]); - } -} diff --git a/test/client_reset_test.dart b/test/client_reset_test.dart index 211ea18781..6fc89cbbbe 100644 --- a/test/client_reset_test.dart +++ b/test/client_reset_test.dart @@ -35,21 +35,21 @@ Future main([List? args]) async { expect( Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: ManualRecoveryHandler((syncError) {}), ).clientResetHandler.clientResyncMode, ClientResyncModeInternal.manual); expect( Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: const DiscardUnsyncedChangesHandler(), ).clientResetHandler.clientResyncMode, ClientResyncModeInternal.discardLocal); expect( Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: const RecoverUnsyncedChangesHandler(), ).clientResetHandler.clientResyncMode, ClientResyncModeInternal.recover); @@ -57,12 +57,12 @@ Future main([List? args]) async { expect( Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: const RecoverOrDiscardUnsyncedChangesHandler(), ).clientResetHandler.clientResyncMode, ClientResyncModeInternal.recoverOrDiscard); - expect(Configuration.flexibleSync(user, [Task.schema, Schedule.schema]).clientResetHandler.clientResyncMode, ClientResyncModeInternal.recoverOrDiscard); + expect(Configuration.flexibleSync(user, getSyncSchema()).clientResetHandler.clientResyncMode, ClientResyncModeInternal.recoverOrDiscard); }); baasTest('ManualRecoveryHandler error is reported in callback', (appConfig) async { @@ -72,7 +72,7 @@ Future main([List? args]) async { final resetCompleter = Completer(); final config = Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: ManualRecoveryHandler((syncError) { resetCompleter.completeError(syncError); }), @@ -93,7 +93,7 @@ Future main([List? args]) async { final resetCompleter = Completer(); final config = Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: ManualRecoveryHandler((clientResetError) { resetCompleter.completeError(clientResetError); }), @@ -122,7 +122,7 @@ Future main([List? args]) async { final resetCompleter = Completer(); final config = Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: ManualRecoveryHandler((clientResetError) { resetCompleter.completeError(clientResetError); }), @@ -152,7 +152,7 @@ Future main([List? args]) async { final user = await getIntegrationUser(app); final onManualResetFallback = Completer(); - final config = Configuration.flexibleSync(user, [Task.schema, Schedule.schema], + final config = Configuration.flexibleSync(user, getSyncSchema(), clientResetHandler: Creator.create( clientResetHandlerType, onBeforeReset: (beforeResetRealm) => throw Exception("This fails!"), @@ -177,7 +177,7 @@ Future main([List? args]) async { throw Exception("This fails!"); } - final config = Configuration.flexibleSync(user, [Task.schema, Schedule.schema], + final config = Configuration.flexibleSync(user, getSyncSchema(), clientResetHandler: Creator.create( clientResetHandlerType, onAfterRecovery: clientResetHandlerType != DiscardUnsyncedChangesHandler ? onAfterReset : null, @@ -204,7 +204,7 @@ Future main([List? args]) async { onAfterCompleter.complete(); } - final config = Configuration.flexibleSync(user, [Task.schema, Schedule.schema], + final config = Configuration.flexibleSync(user, getSyncSchema(), clientResetHandler: Creator.create( clientResetHandlerType, onBeforeReset: (beforeResetRealm) => onBeforeCompleter.complete(), @@ -233,7 +233,7 @@ Future main([List? args]) async { int onAfterRecoveryOccurred = 0; final onAfterCompleter = Completer(); - final config = Configuration.flexibleSync(user, [Task.schema, Schedule.schema], + final config = Configuration.flexibleSync(user, getSyncSchema(), clientResetHandler: Creator.create( clientResetHandlerType, onBeforeReset: (beforeResetRealm) => onBeforeResetOccurred++, @@ -249,21 +249,25 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - + final objectId = ObjectId(); + final addedObjectId = ObjectId(); + final query = realm.query(r'_id IN $0', [ + [objectId, addedObjectId] + ]); realm.subscriptions.update((mutableSubscriptions) { - mutableSubscriptions.add(realm.all()); + mutableSubscriptions.add(query); }); await realm.subscriptions.waitForSynchronization(); await realm.syncSession.waitForDownload(); - final tasksCount = realm.all().length; + final tasksCount = query.length; realm.syncSession.pause(); - realm.write(() => realm.add(Task(ObjectId()))); - expect(tasksCount, lessThan(realm.all().length)); + realm.write(() => realm.add(Task(addedObjectId))); + expect(tasksCount, lessThan(query.length)); final notifications = []; - final subscription = realm.all().changes.listen((event) { + final subscription = query.changes.listen((event) { notifications.add(event); }); @@ -296,24 +300,26 @@ Future main([List? args]) async { final user = await getIntegrationUser(app); final onAfterCompleter = Completer(); - final syncedProduct = Product(ObjectId(), "always synced"); - final maybeProduct = Product(ObjectId(), "maybe synced"); - comparer(Product p1, Product p2) => p1.id == p2.id; - final config = Configuration.flexibleSync(user, [Product.schema], + final syncedId = ObjectId(); + final maybeId = ObjectId(); + + comparer(Product p1, ObjectId expectedId) => p1.id == expectedId; + + final config = Configuration.flexibleSync(user, getSyncSchema(), clientResetHandler: Creator.create( clientResetHandlerType, onBeforeReset: (beforeResetRealm) { - _checkProducts(beforeResetRealm, comparer, expectedList: [syncedProduct, maybeProduct]); + _checkProducts(beforeResetRealm, comparer, expectedList: [syncedId, maybeId]); }, onAfterRecovery: (beforeResetRealm, afterResetRealm) { - _checkProducts(beforeResetRealm, comparer, expectedList: [syncedProduct, maybeProduct]); - _checkProducts(afterResetRealm, comparer, expectedList: [syncedProduct, maybeProduct]); + _checkProducts(beforeResetRealm, comparer, expectedList: [syncedId, maybeId]); + _checkProducts(afterResetRealm, comparer, expectedList: [syncedId, maybeId]); onAfterCompleter.complete(); }, onAfterDiscard: (beforeResetRealm, afterResetRealm) { - _checkProducts(beforeResetRealm, comparer, expectedList: [syncedProduct, maybeProduct]); - _checkProducts(afterResetRealm, comparer, expectedList: [syncedProduct], notExpectedList: [maybeProduct]); + _checkProducts(beforeResetRealm, comparer, expectedList: [syncedId, maybeId]); + _checkProducts(afterResetRealm, comparer, expectedList: [syncedId], notExpectedList: [maybeId]); onAfterCompleter.complete(); }, onManualResetFallback: (clientResetError) => onAfterCompleter.completeError(clientResetError), @@ -321,15 +327,17 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); realm.subscriptions.update((mutableSubscriptions) { - mutableSubscriptions.add(realm.all()); + mutableSubscriptions.add(realm.query(r'_id IN $0', [ + [syncedId, maybeId] + ])); }); await realm.subscriptions.waitForSynchronization(); - realm.write(() => realm.add(syncedProduct)); + realm.write(() => realm.add(Product(syncedId, "always synced"))); await realm.syncSession.waitForUpload(); realm.syncSession.pause(); - realm.write(() => realm.add(maybeProduct)); + realm.write(() => realm.add(Product(maybeId, "maybe synced"))); await triggerClientReset(realm, restartSession: false); realm.syncSession.resume(); @@ -349,7 +357,7 @@ Future main([List? args]) async { bool recovery = false; bool discard = false; - final config = Configuration.flexibleSync(user, [Task.schema, Schedule.schema], + final config = Configuration.flexibleSync(user, getSyncSchema(), clientResetHandler: RecoverOrDiscardUnsyncedChangesHandler( onBeforeReset: (beforeResetRealm) => onBeforeCompleter.complete(), onAfterRecovery: (Realm beforeResetRealm, Realm afterResetRealm) { @@ -389,7 +397,7 @@ Future main([List? args]) async { final config = Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: DiscardUnsyncedChangesHandler( onBeforeReset: (beforeResetRealm) async { await Future.delayed(Duration(seconds: 1)); @@ -426,7 +434,7 @@ Future main([List? args]) async { late ClientResetError clientResetErrorOnManualFallback; final config = Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: DiscardUnsyncedChangesHandler( onBeforeReset: (beforeResetRealm) { onBeforeResetOccurred++; @@ -457,9 +465,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.message, isNotEmpty); }); // 1. userA adds [task0, task1, task2] and syncs it, then disconnects @@ -480,10 +486,10 @@ Future main([List? args]) async { final task1Id = ObjectId(); final task2Id = ObjectId(); final task3Id = ObjectId(); - + List filterByIds = [task0Id, task1Id, task2Id, task3Id]; comparer(Task t1, ObjectId id) => t1.id == id; - final configA = Configuration.flexibleSync(userA, [Task.schema], clientResetHandler: RecoverUnsyncedChangesHandler( + final configA = Configuration.flexibleSync(userA, getSyncSchema(), clientResetHandler: RecoverUnsyncedChangesHandler( onAfterReset: (beforeResetRealm, afterResetRealm) { try { _checkProducts(beforeResetRealm, comparer, expectedList: [task0Id, task1Id], notExpectedList: [task2Id, task3Id]); @@ -495,7 +501,7 @@ Future main([List? args]) async { }, )); - final configB = Configuration.flexibleSync(userB, [Schedule.schema, Task.schema], clientResetHandler: RecoverUnsyncedChangesHandler( + final configB = Configuration.flexibleSync(userB, getSyncSchema(), clientResetHandler: RecoverUnsyncedChangesHandler( onAfterReset: (beforeResetRealm, afterResetRealm) { try { _checkProducts(beforeResetRealm, comparer, expectedList: [task0Id, task1Id, task2Id, task3Id]); @@ -507,8 +513,8 @@ Future main([List? args]) async { }, )); - final realmA = await _syncRealmForUser(configA, [Task(task0Id), Task(task1Id), Task(task2Id)]); - final realmB = await _syncRealmForUser(configB); + final realmA = await _syncRealmForUser(configA, filterByIds, [Task(task0Id), Task(task1Id), Task(task2Id)]); + final realmB = await _syncRealmForUser(configB, filterByIds); realmA.syncSession.pause(); realmB.syncSession.pause(); @@ -542,7 +548,7 @@ Future main([List? args]) async { late ClientResetError clientResetError; final config = Configuration.flexibleSync( user, - [Task.schema, Schedule.schema], + getSyncSchema(), clientResetHandler: ManualRecoveryHandler((syncError) { clientResetError = syncError; resetCompleter.complete(); @@ -554,21 +560,16 @@ 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.isFatal, isTrue); + expect(clientResetError.message, isNotEmpty); - expect(clientResetError.detailedMessage, isNotEmpty); - expect(clientResetError.message == clientResetError.detailedMessage, isFalse); expect(clientResetError.backupFilePath, isNotEmpty); }); } -Future _syncRealmForUser(FlexibleSyncConfiguration config, [List? items]) async { +Future _syncRealmForUser(FlexibleSyncConfiguration config, List filterByIds, [List? items]) async { final realm = getRealm(config); realm.subscriptions.update((mutableSubscriptions) { - mutableSubscriptions.add(realm.all()); + mutableSubscriptions.add(realm.query(r'_id IN $0', [filterByIds])); }); await realm.subscriptions.waitForSynchronization(); @@ -581,8 +582,8 @@ Future _syncRealmForUser(FlexibleSyncConfiguration return realm; } -void _checkProducts(Realm realm, bool Function(T, O) truePredicate, - {required List expectedList, List? notExpectedList}) { +void _checkProducts(Realm realm, bool Function(T, ObjectId) truePredicate, + {required List expectedList, List? notExpectedList}) { final all = realm.all(); for (var expected in expectedList) { if (!all.any((p) => truePredicate(p, expected))) { diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 93b80494fd..94a3884b36 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -82,7 +82,7 @@ Future main([List? args]) async { var customDefaultRealmName = "myRealmName.realm"; Configuration.defaultRealmName = customDefaultRealmName; - var config = Configuration.flexibleSync(user, [Task.schema]); + var config = Configuration.flexibleSync(user, getSyncSchema()); expect(path.basename(config.path), path.basename(customDefaultRealmName)); var realm = getRealm(config); @@ -91,7 +91,7 @@ Future main([List? args]) async { //set a new defaultRealmName customDefaultRealmName = "anotherRealmName.realm"; Configuration.defaultRealmName = customDefaultRealmName; - config = Configuration.flexibleSync(user, [Task.schema]); + config = Configuration.flexibleSync(user, getSyncSchema()); realm = getRealm(config); expect(path.basename(realm.config.path), customDefaultRealmName); }); @@ -108,7 +108,7 @@ Future main([List? args]) async { var app = App(appConfig); var user = await app.logIn(Credentials.anonymous()); - var config = Configuration.flexibleSync(user, [Task.schema]); + var config = Configuration.flexibleSync(user, getSyncSchema()); expect(path.dirname(config.path), startsWith(path.dirname(customDefaultRealmPath))); var realm = getRealm(config); @@ -125,7 +125,7 @@ Future main([List? args]) async { app = App(appConfig); user = await app.logIn(Credentials.anonymous()); - config = Configuration.flexibleSync(user, [Task.schema]); + config = Configuration.flexibleSync(user, getSyncSchema()); realm = getRealm(config); expect(path.dirname(realm.config.path), startsWith(path.dirname(customDefaultRealmPath))); }); @@ -497,7 +497,7 @@ Future main([List? args]) async { final user = await app.logIn(Credentials.emailPassword(testUsername, testPassword)); var invoked = false; - var config = Configuration.flexibleSync(user, [Event.schema], shouldCompactCallback: (totalSize, usedSize) { + var config = Configuration.flexibleSync(user, getSyncSchema(), shouldCompactCallback: (totalSize, usedSize) { invoked = true; return false; }); @@ -510,7 +510,7 @@ Future main([List? args]) async { final app = App(appConfig); final user = await app.logIn(Credentials.emailPassword(testUsername, testPassword)); - final config = Configuration.flexibleSync(user, [Car.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); expect(config.path, contains(user.id)); expect(config.path, contains(appConfig.appId)); @@ -520,7 +520,7 @@ Future main([List? args]) async { final app = App(appConfig); final user = await app.logIn(Credentials.emailPassword(testUsername, testPassword)); - final config = Configuration.flexibleSync(user, [Car.schema], path: 'my-custom-path.realm'); + final config = Configuration.flexibleSync(user, getSyncSchema(), path: 'my-custom-path.realm'); expect(config.path, 'my-custom-path.realm'); }); @@ -532,7 +532,7 @@ Future main([List? args]) async { path.dirname(Configuration.defaultStoragePath), path.basename('my-custom-realm-name.realm'), ); - final config = Configuration.flexibleSync(user, [Event.schema], path: customPath); + final config = Configuration.flexibleSync(user, getSyncSchema(), path: customPath); var realm = getRealm(config); }); @@ -543,8 +543,7 @@ Future main([List? args]) async { final dir = await Directory.systemTemp.createTemp(); final realmPath = path.join(dir.path, 'test.realm'); - final schema = [Task.schema]; - final flexibleSyncConfig = Configuration.flexibleSync(user, schema, path: realmPath); + final flexibleSyncConfig = Configuration.flexibleSync(user, getSyncSchema(), path: realmPath); final realm = getRealm(flexibleSyncConfig); final oid = ObjectId(); realm.subscriptions.update((mutableSubscriptions) { @@ -553,7 +552,7 @@ Future main([List? args]) async { realm.write(() => realm.add(Task(oid))); realm.close(); - final disconnectedSyncConfig = Configuration.disconnectedSync(schema, path: realmPath); + final disconnectedSyncConfig = Configuration.disconnectedSync([Task.schema], path: realmPath); final disconnectedRealm = getRealm(disconnectedSyncConfig); expect(disconnectedRealm.find(oid), isNotNull); }); @@ -587,7 +586,7 @@ Future main([List? args]) async { List key = List.generate(encryptionKeySize + 10, (i) => random.nextInt(256)); expect( - () => Configuration.flexibleSync(user, [Task.schema], encryptionKey: key), + () => Configuration.flexibleSync(user, getSyncSchema(), encryptionKey: key), throws("Wrong encryption key size"), ); }); @@ -611,7 +610,7 @@ Future main([List? args]) async { final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final config = Configuration.flexibleSync(user, [Task.schema], maxNumberOfActiveVersions: 1); + final config = Configuration.flexibleSync(user, getSyncSchema(), maxNumberOfActiveVersions: 1); final realm = await getRealmAsync(config); // First writing to the Realm when opening realm.subscriptions.update((mutableSubscriptions) => mutableSubscriptions.add(realm.all())); expect(() => realm.write(() {}), throws("in the Realm exceeded the limit of 1")); @@ -621,7 +620,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final config = Configuration.flexibleSync(user, [Task.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); final disconnectedConfig = Configuration.disconnectedSync([Task.schema], path: config.path, maxNumberOfActiveVersions: 1); final realm = getRealm(disconnectedConfig); // First writing to the Realm when opening diff --git a/test/credentials_test.dart b/test/credentials_test.dart index 78f1d29f85..13cec2e9d8 100644 --- a/test/credentials_test.dart +++ b/test/credentials_test.dart @@ -38,8 +38,10 @@ Future main([List? args]) async { expect(user1, user2); expect(user1, isNot(user3)); - expect(user1.provider, AuthProviderType.anonymous); - expect(user3.provider, AuthProviderType.anonymous); + expect(user1.identities.length, 1); + expect(user1.identities.first.provider, AuthProviderType.anonymous); + expect(user3.identities.length, 1); + expect(user3.identities.first.provider, AuthProviderType.anonymous); }); test('Credentials email/password', () { @@ -208,7 +210,7 @@ Future main([List? args]) async { expect(user.state, UserState.loggedIn); expect(user.identities[0].id, userId); - expect(user.provider, AuthProviderType.jwt); + expect(user.identities[0].provider, AuthProviderType.jwt); expect(user.profile.email, username); expect(user.profile.name, username); expect(user.profile.gender, "male"); @@ -261,7 +263,7 @@ Future main([List? args]) async { var userId = emailIdentity.id; expect(user.state, UserState.loggedIn); - expect(user.provider, AuthProviderType.emailPassword); + expect(user.identities.first.provider, AuthProviderType.emailPassword); expect(user.profile.email, username); expect(user.profile.name, isNull); expect(user.profile.gender, isNull); @@ -285,7 +287,6 @@ Future main([List? args]) async { expect(jwtUser.state, UserState.loggedIn); expect(jwtUser.identities.singleWhere((identity) => identity.provider == AuthProviderType.jwt).id, jwtUserId); expect(jwtUser.identities.singleWhere((identity) => identity.provider == AuthProviderType.emailPassword).id, userId); - expect(jwtUser.provider, AuthProviderType.jwt); expect(jwtUser.profile.email, username); expect(jwtUser.profile.name, username); expect(jwtUser.profile.gender, "male"); @@ -335,7 +336,6 @@ Future main([List? args]) async { final credentials = Credentials.function(payload); final user = await app.logIn(credentials); expect(user.identities[0].id, userId); - expect(user.provider, AuthProviderType.function); expect(user.identities[0].provider, AuthProviderType.function); }); @@ -347,14 +347,14 @@ Future main([List? args]) async { final credentials = Credentials.function(payload); final user = await app.logIn(credentials); expect(user.identities[0].id, userId); - expect(user.provider, AuthProviderType.function); + expect(user.identities[0].provider, AuthProviderType.function); user.logOut(); final sameUser = await app.logIn(credentials); expect(sameUser.id, user.id); expect(sameUser.identities[0].id, userId); - expect(sameUser.provider, AuthProviderType.function); + expect(sameUser.identities[0].provider, AuthProviderType.function); }); test('Credentials providers', () { diff --git a/test/embedded_test.dart b/test/embedded_test.dart index 656c52180c..d583748094 100644 --- a/test/embedded_test.dart +++ b/test/embedded_test.dart @@ -16,8 +16,6 @@ // //////////////////////////////////////////////////////////////////////////////// -import 'dart:typed_data'; - import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; @@ -36,14 +34,6 @@ Future main([List? args]) async { return getRealm(config); } - Future getSyncRealm(AppConfiguration config) async { - final app = App(config); - final user = await getAnonymousUser(app); - final realmConfig = Configuration.flexibleSync( - user, [AllTypesEmbedded.schema, ObjectWithEmbedded.schema, RecursiveEmbedded1.schema, RecursiveEmbedded2.schema, RecursiveEmbedded3.schema]); - return getRealm(realmConfig); - } - test('Local Realm with orphan embedded schemas works', () { final config = Configuration.local([AllTypesEmbedded.schema]); final realm = getRealm(config); @@ -58,10 +48,10 @@ Future main([List? args]) async { baasTest('Synchronized Realm with orphan embedded schemas throws', (configuration) async { final app = App(configuration); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, [AllTypesEmbedded.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); expect(() => getRealm(config), throws("Embedded object 'AllTypesEmbedded' is unreachable by any link path from top level objects")); - }); + }, skip: "This test requires a new app service with missing embedded parent schema."); test('Embedded object roundtrip', () { final realm = getLocalRealm(); @@ -400,7 +390,7 @@ Future main([List? args]) async { }); baasTest('Embedded objects synchronization', (config) async { - final realm1 = await getSyncRealm(config); + final realm1 = await getIntegrationRealm(appConfig: config); final differentiator = Uuid.v4(); realm1.subscriptions.update((mutableSubscriptions) { @@ -417,7 +407,7 @@ Future main([List? args]) async { await realm1.subscriptions.waitForSynchronization(); await realm1.syncSession.waitForUpload(); - final realm2 = await getSyncRealm(config); + final realm2 = await getIntegrationRealm(appConfig: config); realm2.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.add(realm2.query(r'differentiator = $0', [differentiator])); }); diff --git a/test/list_test.dart b/test/list_test.dart index 7644396aa4..f93dab8a72 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -1233,7 +1233,7 @@ Future main([List? args]) async { playersAsResults.changes, emitsInOrder([ isA>().having((changes) => changes.inserted, 'inserted', []), // always an empty event on subscription - isA>().having((changes) => changes.isCleared, 'isCleared', true), + isA>().having((changes) => changes.results.isEmpty, 'isCleared', true), ])); realm.write(() => team.players.clear()); expect(playersAsResults.length, 0); diff --git a/test/manual_test.dart b/test/manual_test.dart index b5c0922df7..3847bb53ab 100644 --- a/test/manual_test.dart +++ b/test/manual_test.dart @@ -148,7 +148,7 @@ Future main([List? args]) async { final credentials = Credentials.facebook(accessToken); final user = await app.logIn(credentials); expect(user.state, UserState.loggedIn); - expect(user.provider, AuthProviderType.facebook); + expect(user.identities[0].provider, AuthProviderType.facebook); expect(user.profile.name, "Open Graph Test User"); }, skip: "Manual test"); }, skip: "Manual tests"); diff --git a/test/realm_logger_test.dart b/test/realm_logger_test.dart index 3903d10eb8..7d527aceae 100644 --- a/test/realm_logger_test.dart +++ b/test/realm_logger_test.dart @@ -212,11 +212,14 @@ Future main([List? args]) async { return result; }); + final expected = [ + const LoggedMessage(RealmLogLevel.error, "2"), + const LoggedMessage(RealmLogLevel.error, "2"), + const LoggedMessage(RealmLogLevel.error, "first only"), + const LoggedMessage(RealmLogLevel.trace, "3") + ]; + //first isolate should have collected all the messages - expect(actual.length, 4); - expect(actual[0], const LoggedMessage(RealmLogLevel.error, "2")); - expect(actual[1], const LoggedMessage(RealmLogLevel.error, "2")); - expect(actual[2], const LoggedMessage(RealmLogLevel.error, "first only")); - expect(actual[3], const LoggedMessage(RealmLogLevel.trace, "3")); + expect(actual, expected); }); } diff --git a/test/realm_set_test.dart b/test/realm_set_test.dart index ac85116d7a..e99df0d6ec 100644 --- a/test/realm_set_test.dart +++ b/test/realm_set_test.dart @@ -565,9 +565,6 @@ Future main([List? args]) async { }); test('RealmSet<$type> basic operations on unmanaged sets', () { - var config = Configuration.local([TestRealmSets.schema, Car.schema]); - var realm = getRealm(config); - var testSet = TestRealmSets(1); var set = testSet.setByType(type).set; var values = testSet.setByType(type).values; @@ -716,7 +713,7 @@ Future main([List? args]) async { carsResult.changes, emitsInOrder([ isA>().having((changes) => changes.inserted, 'inserted', []), // always an empty event on subscription - isA>().having((changes) => changes.isCleared, 'isCleared', true), + isA>().having((changes) => changes.results.isEmpty, 'isCleared', true), ])); realm.write(() => testSets.objectsSet.clear()); }); @@ -801,7 +798,7 @@ Future main([List? args]) async { if (count > 1) fail('Should only receive one event'); } }); - + test('Query on RealmSet with IN-operator', () { var config = Configuration.local([TestRealmSets.schema, Car.schema]); var realm = getRealm(config); diff --git a/test/realm_test.dart b/test/realm_test.dart index 09c17a3cce..ebd1845521 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -564,7 +564,7 @@ Future main([List? args]) async { // Wait for realm2 to see the changes. This would not be necessary if we // cache native instances. - await Future.delayed(Duration(milliseconds: 1)); + await Future.delayed(Duration(milliseconds: 10)); expect(realm2.all().length, 1); expect(realm2.all().single.name, "Peter"); @@ -995,12 +995,12 @@ Future main([List? args]) async { final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); List key = List.generate(encryptionKeySize, (i) => random.nextInt(256)); - final configuration = Configuration.flexibleSync(user, [Task.schema], encryptionKey: key); + final configuration = Configuration.flexibleSync(user, getSyncSchema(), encryptionKey: key); final realm = getRealm(configuration); expect(realm.isClosed, false); expect( - () => getRealm(Configuration.flexibleSync(user, [Task.schema])), + () => getRealm(Configuration.flexibleSync(user, getSyncSchema())), throws("already opened with a different encryption key"), ); }); @@ -1017,7 +1017,7 @@ Future main([List? args]) async { Future.delayed(Duration(milliseconds: 10), () => transaction.commit()); final transaction1 = await realm.beginWriteAsync(); - + await transaction1.commitAsync(); expect(transaction.isOpen, false); @@ -1231,7 +1231,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); final realm = await getRealmAsync(configuration); expect(realm.isClosed, false); @@ -1254,7 +1254,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); final cancellationToken = CancellationToken(); cancellationToken.cancel(); @@ -1265,7 +1265,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); final cancellationToken = CancellationToken(); final isRealmCancelled = getRealmAsync(configuration, cancellationToken: cancellationToken).isCancelled(); @@ -1277,7 +1277,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); final cancellationToken = CancellationToken(); @@ -1292,7 +1292,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); final cancellationToken1 = CancellationToken(); final isRealm1Cancelled = getRealmAsync(configuration, cancellationToken: cancellationToken1).isCancelled(); @@ -1308,12 +1308,12 @@ Future main([List? args]) async { final app = App(appConfiguration); final user1 = await app.logIn(Credentials.anonymous()); - final configuration1 = Configuration.flexibleSync(user1, [Task.schema]); + final configuration1 = Configuration.flexibleSync(user1, getSyncSchema()); final cancellationToken1 = CancellationToken(); final isRealm1Cancelled = getRealmAsync(configuration1, cancellationToken: cancellationToken1).isCancelled(); final user2 = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final configuration2 = Configuration.flexibleSync(user2, [Task.schema]); + final configuration2 = Configuration.flexibleSync(user2, getSyncSchema()); final cancellationToken2 = CancellationToken(); final isRealm2Cancelled = getRealmAsync(configuration2, cancellationToken: cancellationToken2).isCancelled(); @@ -1326,7 +1326,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); final cancellationToken = CancellationToken(); final realm = await getRealmAsync(configuration, cancellationToken: cancellationToken); @@ -1342,7 +1342,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [Task.schema]); + final configuration = Configuration.flexibleSync(user, getSyncSchema()); int transferredBytes = -1; final completer = Completer(); @@ -1399,8 +1399,7 @@ Future main([List? args]) async { expect(progressReturned, isFalse); }); - const compactTest = "compact_test"; - void addDataForCompact(Realm realm) { + void addDataForCompact(Realm realm, String compactTest) { realm.write(() { for (var i = 0; i < 2500; i++) { realm.add(Product(ObjectId(), compactTest)); @@ -1418,6 +1417,7 @@ Future main([List? args]) async { Future createRealmForCompact(Configuration config) async { var realm = getRealm(config); + final compactTest = generateRandomString(10); if (config is FlexibleSyncConfiguration) { realm.subscriptions.update((mutableSubscriptions) { @@ -1426,7 +1426,7 @@ Future main([List? args]) async { await realm.subscriptions.waitForSynchronization(); } - addDataForCompact(realm); + addDataForCompact(realm, compactTest); if (config is FlexibleSyncConfiguration) { await realm.syncSession.waitForDownload(); @@ -1533,41 +1533,34 @@ Future main([List? args]) async { baasTest('Realm - synced realm can be compacted', (appConfiguration) async { final app = App(appConfiguration); - final credentials = Credentials.anonymous(); + final credentials = Credentials.anonymous(reuseCredentials: false); var user = await app.logIn(credentials); final path = p.join(Configuration.defaultStoragePath, "${generateRandomString(8)}.realm"); - final config = Configuration.flexibleSync(user, [Product.schema], path: path); + final config = Configuration.flexibleSync(user, getSyncSchema(), path: path); final beforeCompactSize = await createRealmForCompact(config); - user.logOut(); - Future.delayed(Duration(seconds: 5)); - - final compacted = Realm.compact(config); + final compacted = await runWithRetries(() => Realm.compact(config)); validateCompact(compacted, config.path, beforeCompactSize); //test the realm can be opened. - final realm = getRealm(Configuration.disconnectedSync([Product.schema], path: path)); - }); + final realm = getRealm(config); + }, appName: AppNames.autoConfirm); baasTest('Realm - synced encrypted realm can be compacted', (appConfiguration) async { final app = App(appConfiguration); - final credentials = Credentials.anonymous(); - final path = p.join(Configuration.defaultStoragePath, "${generateRandomString(8)}.realm"); + final credentials = Credentials.anonymous(reuseCredentials: false); var user = await app.logIn(credentials); List key = List.generate(encryptionKeySize, (i) => random.nextInt(256)); - final config = Configuration.flexibleSync(user, [Product.schema], encryptionKey: key, path: path); + final path = p.join(Configuration.defaultStoragePath, "${generateRandomString(8)}.realm"); + final config = Configuration.flexibleSync(user, getSyncSchema(), encryptionKey: key, path: path); final beforeCompactSize = await createRealmForCompact(config); - user.logOut(); - Future.delayed(Duration(seconds: 5)); - - final compacted = Realm.compact(config); + final compacted = await runWithRetries(() => Realm.compact(config)); validateCompact(compacted, config.path, beforeCompactSize); - user = await app.logIn(credentials); //test the realm can be opened. - final realm = getRealm(Configuration.disconnectedSync([Product.schema], path: path, encryptionKey: key)); - }); + final realm = getRealm(config); + }, appName: AppNames.autoConfirm); test('Realm writeCopy local to existing file', () { final config = Configuration.local([Car.schema]); @@ -1593,7 +1586,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final credentials = Credentials.anonymous(reuseCredentials: false); var user = await app.logIn(credentials); - final configCopy = Configuration.flexibleSync(user, [Product.schema]); + final configCopy = Configuration.flexibleSync(user, getSyncSchema()); expect(() => originalRealm.writeCopy(configCopy), throws("Realm cannot be converted to a flexible sync realm unless flexible sync is already enabled")); }); @@ -1709,14 +1702,14 @@ Future main([List? args]) async { baasTest('Realm writeCopy Sync->Sync - $testDescription can be opened and synced', (appConfiguration) async { final app = App(appConfiguration); var user1 = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final originalConfig = Configuration.flexibleSync(user1, [Product.schema], encryptionKey: sourceEncryptedKey); + final originalConfig = Configuration.flexibleSync(user1, getSyncSchema(), encryptionKey: sourceEncryptedKey); final originalRealm = getRealm(originalConfig); var itemsCount = 2; final productNamePrefix = generateRandomString(10); await _addDataToAtlas(originalRealm, productNamePrefix, itemsCount: itemsCount); var user2 = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final configCopy = Configuration.flexibleSync(user2, [Product.schema], encryptionKey: destinationEncryptedKey); + final configCopy = Configuration.flexibleSync(user2, getSyncSchema(), encryptionKey: destinationEncryptedKey); originalRealm.writeCopy(configCopy); originalRealm.close(); @@ -1737,7 +1730,7 @@ Future main([List? args]) async { // Create another user's realm and download the data var anotherUser = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final anotherUserRealm = getRealm(Configuration.flexibleSync(anotherUser, [Product.schema])); + final anotherUserRealm = getRealm(Configuration.flexibleSync(anotherUser, getSyncSchema())); await _addSubscriptions(anotherUserRealm, productNamePrefix); await anotherUserRealm.syncSession.waitForUpload(); await anotherUserRealm.syncSession.waitForDownload(); @@ -1757,7 +1750,7 @@ Future main([List? args]) async { baasTest('Realm writeCopy Sync->Local - $testDescription can be opened and synced', (appConfiguration) async { final app = App(appConfiguration); var user = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final originalConfig = Configuration.flexibleSync(user, [Product.schema], encryptionKey: sourceEncryptedKey); + final originalConfig = Configuration.flexibleSync(user, getSyncSchema(), encryptionKey: sourceEncryptedKey); final originalRealm = getRealm(originalConfig); var itemsCount = 2; final productNamePrefix = generateRandomString(10); @@ -1896,7 +1889,7 @@ Future main([List? args]) async { final app = App(appConfiguration); final productName = generateRandomUnicodeString(); final user = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final config = Configuration.flexibleSync(user, [Product.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); final realm = getRealm(config); await _addSubscriptions(realm, productName); realm.write(() => realm.add(Product(ObjectId(), productName))); @@ -1954,13 +1947,13 @@ extension on Future { Future _subscribeForAtlasAddedData(App app, {String? queryDifferentiator, int itemsCount = 100}) async { final productNamePrefix = queryDifferentiator ?? generateRandomString(10); final user1 = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final config1 = Configuration.flexibleSync(user1, [Product.schema]); + final config1 = Configuration.flexibleSync(user1, getSyncSchema()); final realm1 = getRealm(config1); await _addSubscriptions(realm1, productNamePrefix); realm1.close(); final user2 = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final config2 = Configuration.flexibleSync(user2, [Product.schema]); + final config2 = Configuration.flexibleSync(user2, getSyncSchema()); final realm2 = getRealm(config2); await _addDataToAtlas(realm2, productNamePrefix, itemsCount: itemsCount); realm2.close(); diff --git a/test/session_test.dart b/test/session_test.dart index 27e1a4d361..2c60938a26 100644 --- a/test/session_test.dart +++ b/test/session_test.dart @@ -17,10 +17,8 @@ //////////////////////////////////////////////////////////////////////////////// import 'dart:async'; - import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; -import '../lib/src/session.dart' show SessionInternal; import 'test.dart'; Future main([List? args]) async { @@ -43,7 +41,7 @@ Future main([List? args]) async { baasTest('SyncSession.user returns a valid user', (configuration) async { final app = App(configuration); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, [Task.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); final realm = getRealm(config); expect(realm.syncSession.user, user); @@ -55,7 +53,7 @@ Future main([List? args]) async { baasTest('SyncSession when isolate is torn down does not crash', (configuration) async { final app = App(configuration); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, [Task.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); // Don't use getRealm because we want the Realm to survive final realm = Realm(config); @@ -279,41 +277,6 @@ Future main([List? args]) async { await downloadData.subscription.cancel(); }); - baasTest('SyncSession test 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 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)"); - }); - - final realm = getRealm(config); - - realm.syncSession.raiseError(SyncErrorCategory.session, SyncSessionErrorCode.badAuthentication.code, 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)"); - }); - final realm = getRealm(config); - - realm.syncSession.raiseError(SyncErrorCategory.client, SyncClientErrorCode.badChangeset.code, true); - }); - baasTest('SyncSession.getConnectionStateStream', (configuration) async { final realm = await getIntegrationRealm(); @@ -355,7 +318,7 @@ Future main([List? args]) async { 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 config = Configuration.flexibleSync(user, getSyncSchema()); final realm = getRealm(config); final session = realm.syncSession; @@ -365,28 +328,6 @@ 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); - }); - } } class StreamProgressData { diff --git a/test/subscription_test.dart b/test/subscription_test.dart index fbbae0e9ce..75cf7441bb 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -35,12 +35,7 @@ void testSubscriptions(String name, FutureOr Function(Realm) testFunc) asy final app = App(appConfiguration); final credentials = Credentials.anonymous(); final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, [ - Task.schema, - Schedule.schema, - Event.schema, - ]) - ..sessionStopPolicy = SessionStopPolicy.immediately; + final configuration = Configuration.flexibleSync(user, getSyncSchema())..sessionStopPolicy = SessionStopPolicy.immediately; final realm = getRealm(configuration); await testFunc(realm); }); @@ -482,26 +477,22 @@ Future main([List? args]) async { final userX = await appX.logIn(credentials); final userY = await appY.logIn(credentials); - final realmX = getRealm(Configuration.flexibleSync(userX, [Task.schema])); - final realmY = getRealm(Configuration.flexibleSync(userY, [Task.schema])); + final realmX = getRealm(Configuration.flexibleSync(userX, getSyncSchema())); + final objectId = ObjectId(); realmX.subscriptions.update((mutableSubscriptions) { - mutableSubscriptions.add(realmX.all()); + mutableSubscriptions.add(realmX.query(r'_id == $0', [objectId])); }); - - final objectId = ObjectId(); + await realmX.subscriptions.waitForSynchronization(); realmX.write(() => realmX.add(Task(objectId))); + await realmX.syncSession.waitForUpload(); + final realmY = getRealm(Configuration.flexibleSync(userY, getSyncSchema())); realmY.subscriptions.update((mutableSubscriptions) { - mutableSubscriptions.add(realmY.all()); + mutableSubscriptions.add(realmY.query(r'_id == $0', [objectId])); }); - - await realmX.subscriptions.waitForSynchronization(); await realmY.subscriptions.waitForSynchronization(); - - await realmX.syncSession.waitForUpload(); await realmY.syncSession.waitForDownload(); - final task = realmY.find(objectId); expect(task, isNotNull); }); @@ -510,10 +501,7 @@ Future main([List? args]) async { final app = App(configuration); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync( - user, - [Task.schema], - ); + final config = Configuration.flexibleSync(user, getSyncSchema()); final realm = getRealm(config); expect(() => realm.write(() => realm.add(Task(ObjectId()))), throws("no flexible sync subscription has been created")); @@ -553,7 +541,7 @@ Future main([List? args]) async { final app = App(configuration); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, [Task.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); final realm = getRealm(config); final subscriptions = realm.subscriptions; @@ -568,7 +556,7 @@ Future main([List? args]) async { final productNamePrefix = generateRandomString(4); final app = App(configuration); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, [Product.schema], syncErrorHandler: (syncError) { + final config = Configuration.flexibleSync(user, getSyncSchema(), syncErrorHandler: (syncError) { compensatingWriteError = syncError; }); final realm = getRealm(config); @@ -581,13 +569,8 @@ Future main([List? args]) async { await realm.syncSession.waitForUpload(); 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'), - isTrue); - expect(sessionError.detailedMessage, isNotEmpty); - expect(sessionError.message == sessionError.detailedMessage, isFalse); + final sessionError = compensatingWriteError as CompensatingWriteError; + expect(sessionError.message, startsWith('Client attempted a write that is not allowed')); expect(sessionError.compensatingWrites, isNotNull); final writeReason = sessionError.compensatingWrites!.first; expect(writeReason, isNotNull); diff --git a/test/test.dart b/test/test.dart index da73cca3bc..8f16db5767 100644 --- a/test/test.dart +++ b/test/test.dart @@ -327,6 +327,29 @@ class _ObjectWithDecimal { Decimal128? nullableDecimal; } +@RealmModel(ObjectType.asymmetricObject) +class _Asymmetric { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; + + late List<_Embedded> embeddedObjects; +} + +@RealmModel(ObjectType.embeddedObject) +class _Embedded { + late int value; + late RealmValue any; + _Symmetric? symmetric; +} + +@RealmModel() +class _Symmetric { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; +} + String? testName; Map arguments = {}; final baasApps = {}; @@ -393,7 +416,7 @@ Future setupTests(List? args) async { setUp(() { Realm.logger = Logger.detached('test run') - ..level = Level.INFO + ..level = Level.ALL ..onRecord.listen((record) { testing.printOnFailure('${record.time} ${record.level.name}: ${record.message}'); }); @@ -404,7 +427,7 @@ Future setupTests(List? args) async { addTearDown(() async { final paths = HashSet(); paths.add(path); - + realmCore.clearCachedApps(); while (_openRealms.isNotEmpty) { @@ -680,11 +703,11 @@ Future createServerApiKey(App app, String name, {bool enabled = true}) a return await client.createApiKey(baasApp, name, enabled); } -Future getIntegrationRealm({App? app, ObjectId? differentiator}) async { - app ??= App(await getAppConfig()); +Future getIntegrationRealm({App? app, ObjectId? differentiator, AppConfiguration? appConfig}) async { + app ??= App(appConfig ?? await getAppConfig()); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, [Task.schema, Schedule.schema, NullableTypes.schema]); + final config = Configuration.flexibleSync(user, getSyncSchema()); final realm = getRealm(config); if (differentiator != null) { realm.subscriptions.update((mutableSubscriptions) { @@ -809,3 +832,43 @@ void printSplunkLogLink(AppNames appName, String? uriVariable) { "https://splunk.corp.mongodb.com/en-US/app/search/search?q=search index=baas$host \"${app.uniqueName}-*\" | reverse | top error msg&earliest=-7d&latest=now&display.general.type=visualizations"); print("Splunk logs: $splunk"); } + +/// Schema list for default app service +/// used for all the flexible sync tests. +/// The full list of schemas is required when creating +/// a flexibleSync configuration to the default app service +/// to avoid causing breaking changes in development mode. +List getSyncSchema() { + return [ + Task.schema, + Schedule.schema, + Product.schema, + Event.schema, + AllTypesEmbedded.schema, + ObjectWithEmbedded.schema, + RecursiveEmbedded1.schema, + RecursiveEmbedded2.schema, + RecursiveEmbedded3.schema, + NullableTypes.schema, + Asymmetric.schema, + Embedded.schema, + Symmetric.schema, + ]; +} + +Future runWithRetries(bool Function() tester, {int retryDelay = 100, int attempts = 100}) async { + var success = tester(); + var timeout = retryDelay * attempts; + + while (!success && attempts > 0) { + await Future.delayed(Duration(milliseconds: retryDelay)); + success = tester(); + attempts--; + } + + if (!success) { + throw TimeoutException('Failed to meet condition after $timeout ms.'); + } + + return success; +} diff --git a/test/test.g.dart b/test/test.g.dart index b5d1e074b6..3f7e00d55b 100644 --- a/test/test.g.dart +++ b/test/test.g.dart @@ -2092,3 +2092,138 @@ class ObjectWithDecimal extends _ObjectWithDecimal ]); } } + +// ignore_for_file: type=lint +class Asymmetric extends _Asymmetric + with RealmEntity, RealmObjectBase, AsymmetricObject { + Asymmetric( + ObjectId id, { + Iterable embeddedObjects = const [], + }) { + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set>( + this, 'embeddedObjects', RealmList(embeddedObjects)); + } + + Asymmetric._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + RealmList get embeddedObjects => + RealmObjectBase.get(this, 'embeddedObjects') + as RealmList; + @override + set embeddedObjects(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Asymmetric freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Asymmetric._); + return const SchemaObject( + ObjectType.asymmetricObject, Asymmetric, 'Asymmetric', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + SchemaProperty('embeddedObjects', RealmPropertyType.object, + linkTarget: 'Embedded', collectionType: RealmCollectionType.list), + ]); + } +} + +// ignore_for_file: type=lint +class Embedded extends _Embedded + with RealmEntity, RealmObjectBase, EmbeddedObject { + Embedded( + int value, { + RealmValue any = const RealmValue.nullValue(), + Symmetric? symmetric, + }) { + RealmObjectBase.set(this, 'value', value); + RealmObjectBase.set(this, 'any', any); + RealmObjectBase.set(this, 'symmetric', symmetric); + } + + Embedded._(); + + @override + int get value => RealmObjectBase.get(this, 'value') as int; + @override + set value(int value) => RealmObjectBase.set(this, 'value', value); + + @override + RealmValue get any => + RealmObjectBase.get(this, 'any') as RealmValue; + @override + set any(RealmValue value) => RealmObjectBase.set(this, 'any', value); + + @override + Symmetric? get symmetric => + RealmObjectBase.get(this, 'symmetric') as Symmetric?; + @override + set symmetric(covariant Symmetric? value) => + RealmObjectBase.set(this, 'symmetric', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Embedded freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Embedded._); + return const SchemaObject(ObjectType.embeddedObject, Embedded, 'Embedded', [ + SchemaProperty('value', RealmPropertyType.int), + SchemaProperty('any', RealmPropertyType.mixed, optional: true), + SchemaProperty('symmetric', RealmPropertyType.object, + optional: true, linkTarget: 'Symmetric'), + ]); + } +} + +// ignore_for_file: type=lint +class Symmetric extends _Symmetric + with RealmEntity, RealmObjectBase, RealmObject { + Symmetric( + ObjectId id, + ) { + RealmObjectBase.set(this, '_id', id); + } + + Symmetric._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Symmetric freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Symmetric._); + return const SchemaObject(ObjectType.realmObject, Symmetric, 'Symmetric', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + ]); + } +} diff --git a/test/user_test.dart b/test/user_test.dart index 53958a74fc..0980af2617 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -97,16 +97,6 @@ Future main([List? args]) async { expect(user.deviceId, isNotNull); }); - baasTest('User provider', (configuration) async { - final app = App(configuration); - final credentials = Credentials.anonymous(); - var user = await app.logIn(credentials); - expect(user.provider, AuthProviderType.anonymous); - - user = await app.logIn(Credentials.emailPassword(testUsername, testPassword)); - expect(user.provider, AuthProviderType.emailPassword); - }); - baasTest('User profile', (configuration) async { final app = App(configuration); final user = await app.logIn(Credentials.emailPassword(testUsername, testPassword)); @@ -380,9 +370,7 @@ Future main([List? args]) async { final credentials = Credentials.apiKey(key.value!); final apiKeyUser = await app.logIn(credentials); - expect(apiKeyUser.provider, AuthProviderType.apiKey); expect(apiKeyUser.id, user.id); - expect(apiKeyUser.refreshToken, isNot(user.refreshToken)); }); baasTest('User.apiKeys can login with reenabled key', (configuration) async { @@ -405,9 +393,7 @@ Future main([List? args]) async { await enableAndVerifyApiKey(user, key.id); final apiKeyUser = await app.logIn(credentials); - expect(apiKeyUser.provider, AuthProviderType.apiKey); expect(apiKeyUser.id, user.id); - expect(apiKeyUser.refreshToken, isNot(user.refreshToken)); }); baasTest("User.apiKeys can't login with deleted key", (configuration) async { @@ -452,16 +438,6 @@ Future main([List? args]) async { await expectLater(() => apiKeys.fetchAll(), throws('User must be logged in to fetch all API keys')); }); - baasTest("Credentials.apiKey user cannot access API keys", (configuration) async { - final app = App(configuration); - final user = await getIntegrationUser(app); - final apiKey = await createAndVerifyApiKey(user, 'my-key'); - - final apiKeyUser = await app.logIn(Credentials.apiKey(apiKey.value!)); - - expect(() => apiKeyUser.apiKeys, throws('Users logged in with API key cannot manage API keys')); - }); - baasTest("Credentials.apiKey with server-generated can login user", (configuration) async { final app = App(configuration); @@ -470,7 +446,6 @@ Future main([List? args]) async { final apiKeyUser = await app.logIn(credentials); - expect(apiKeyUser.provider, AuthProviderType.apiKey); expect(apiKeyUser.state, UserState.loggedIn); }); From ddd5ed3afd34b10b4adc88b14e94df10582b53cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 1 Nov 2023 12:47:10 +0100 Subject: [PATCH 05/16] Rudimentary geospatial support (#1389) Co-authored-by: Nikola Irinchev --- .github/workflows/dart-desktop-tests.yml | 2 +- .vscode/settings.json | 4 + CHANGELOG.md | 1 + common/lib/src/realm_types.dart | 246 +++++++++++++++ lib/src/native/realm_core.dart | 10 +- lib/src/realm_class.dart | 9 +- test/geospatial_test.dart | 366 +++++++++++++++++++++++ test/geospatial_test.g.dart | 140 +++++++++ 8 files changed, 775 insertions(+), 3 deletions(-) create mode 100644 test/geospatial_test.dart create mode 100644 test/geospatial_test.g.dart diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index cbce2ac779..b3082b7165 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -90,7 +90,7 @@ jobs: --check-ignore \ --lcov \ --packages .dart_tool/package_config.json \ - --report-on lib + --report-on lib,common lcov --remove ./coverage/lcov.info '*.g.dart' '*/lib/src/cli/*' '*/lib/src/native/realm_bindings.dart' -o coverage/pruned-lcov.info - name: Publish realm_dart coverage diff --git a/.vscode/settings.json b/.vscode/settings.json index 533ea4df16..aeafc22375 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,12 +22,16 @@ "finalizable", "finalizer", "fnum", + "geospatial", "HRESULT", "keepalive", "loggable", + "mugaritz", "nodoc", "nullptr", "posix", + "sublist", + "sublists", "TRUEPREDICATE", "unmanaged", "upsert", diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8a88432b..9eb4c3d96d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## vNext (TBD) ### Enhancements +* Support for performing geo spatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox` and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations ([#1389](https://github.com/realm/realm-dart/pull/1389)) * Suppressing rules for a *.g.dart files ([#1413](https://github.com/realm/realm-dart/pull/1413)) * Full text search supports searching for prefix only. Eg. "description TEXT 'alex*'" (Core upgrade) * Unknown protocol errors received from the baas server will no longer cause the application to crash if a valid error action is also received. (Core upgrade) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index b3c1545ffb..7f48cc2a9a 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////////// import 'dart:ffi'; +import 'dart:math'; import 'dart:typed_data'; import 'package:objectid/objectid.dart'; import 'package:sane_uuid/uuid.dart'; @@ -227,3 +228,248 @@ class RealmValue { @override String toString() => 'RealmValue($value)'; } + +/// A base type for the supported geospatial shapes. +sealed class GeoShape {} + +/// A point on the earth's surface. +/// +/// It cannot be persisted as a property on a realm object. +/// +/// Instead, you must use a custom embedded object with the following structure: +/// ```dart +/// @RealmModel(ObjectType.embeddedObject) +/// class _Location { +/// final String type = 'Point'; +/// final List coordinates = const [0, 0]; +/// +/// // The rest of the class is just convenience methods +/// double get lon => coordinates[0]; +/// set lon(double value) => coordinates[0] = value; +/// +/// double get lat => coordinates[1]; +/// set lat(double value) => coordinates[1] = value; +/// +/// GeoPoint toGeoPoint() => GeoPoint(lon: lon, lat: lat); +/// } +/// ``` +/// You can then use it as a property on a realm object: +/// ```dart +/// @RealmModel() +/// class _Restaurant { +/// @PrimaryKey() +/// late String name; +/// _Location? location; +/// } +/// ``` +/// For convenience add an extension method on [GeoPoint]: +/// ```dart +/// extension on GeoPoint { +/// Location toLocation() { +/// return Location(coordinates: [lon, lat]); +/// } +/// } +/// ``` +/// to easily convert between [GeoPoint]s and `Location`s. +/// +/// The following may also be useful: +/// ```dart +/// extension on (num, num) { +/// GeoPoint toGeoPoint() => GeoPoint(lon: $1.toDouble(), lat: $2.toDouble()); +/// Location toLocation() => toGeoPoint().toLocation(); +/// } +/// ``` +final class GeoPoint implements GeoShape { + final double lon; + final double lat; + + /// Create a point from a [lon]gitude and [lat]gitude. + /// [lon] must be between -180 and 180, and [lat] must be between -90 and 90. + GeoPoint({required this.lon, required this.lat}) { + if (lon < -180 || lon > 180) throw ArgumentError.value(lon, 'lon', 'must be between -180 and 180'); + if (lat < -90 || lat > 90) throw ArgumentError.value(lat, 'lat', 'must be between -90 and 90'); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! GeoPoint) return false; + return lat == other.lat && lon == other.lon; + } + + @override + int get hashCode => Object.hash(lon, lat); + + @override + String toString() => '[$lon, $lat]'; +} + +/// A box on the earth's surface. +/// +/// This type can be used as the query argument for a `geoWithin` query. +/// It cannot be persisted as a property on a realm object. +final class GeoBox implements GeoShape { + final GeoPoint southWest; + final GeoPoint northEast; + + /// Create a box from a [southWest] and a [northEast] point + const GeoBox(this.southWest, this.northEast); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! GeoBox) return false; + return southWest == other.southWest && northEast == other.northEast; + } + + @override + int get hashCode => Object.hash(southWest, northEast); + + @override + String toString() => 'geoBox($southWest, $northEast)'; +} + +typedef GeoRing = List; + +extension on GeoRing { + void validate() { + if (first != last) throw ArgumentError('Vertices must form a ring (first != last)'); + if (length < 4) throw ArgumentError('Ring must have at least 3 different vertices'); + } +} + +/// A polygon on the earth's surface. +/// +/// This type can be used as the query argument for a `geoWithin` query. +/// It cannot be persisted as a property on a realm object. +final class GeoPolygon implements GeoShape { + final GeoRing outerRing; + final List holes; + + /// Create a polygon from an [outerRing] and a list of [holes] + /// The outer ring must be a closed ring, and the holes must be non-overlapping + /// closed rings inside the outer ring. + GeoPolygon(this.outerRing, [this.holes = const []]) { + outerRing.validate(); + for (final hole in holes) { + hole.validate(); + } + } + + @override + operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! GeoPolygon) return false; + return outerRing == other.outerRing && holes == other.holes; + } + + @override + int get hashCode => Object.hash(outerRing, holes); + + @override + String toString() { + ringToString(GeoRing ring) => '{${ring.join(', ')}}'; + + final outerRingString = ringToString(outerRing); + if (holes.isEmpty) return 'geoPolygon($outerRingString)'; + + final holesString = holes.map(ringToString).join(', '); + return 'geoPolygon($outerRingString, $holesString)'; + } +} + +const _metersPerMile = 1609.344; +const _radiansPerMeterOnEarthSphere = 1.5678502891116e-7; // at equator +const _radiansPerDegree = pi / 180; + +/// An equatorial distance on earth's surface. +final class GeoDistance implements Comparable { + /// The distance in radians + final double radians; + + /// Create a distance from radians + const GeoDistance(this.radians); + + /// Create a distance from [meters] + GeoDistance.fromMeters(double meters) : radians = meters * _radiansPerMeterOnEarthSphere; + + /// Create a distance from [degrees] + GeoDistance.fromDegrees(double degrees) : radians = degrees * _radiansPerDegree; + + /// Create a distance from [kilometers] + factory GeoDistance.fromKilometers(double kilometers) => GeoDistance.fromMeters(kilometers * 1000); + + /// Create a distance from [miles] + factory GeoDistance.fromMiles(double miles) => GeoDistance.fromMeters(miles * _metersPerMile); + + /// The distance in degrees + double get degrees => radians / _radiansPerDegree; + + /// The distance in meters + double get meters => radians / _radiansPerMeterOnEarthSphere; + + /// The distance in kilometers + double get kilometers => meters / 1000; + + /// The distance in miles + double get miles => meters / _metersPerMile; + + @override + int compareTo(GeoDistance other) => radians.compareTo(other.radians); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! GeoDistance) return false; + return compareTo(other) == 0; + } + + @override + int get hashCode => radians.hashCode; + + @override + String toString() => '$radians'; +} + +/// Convert a [num] to a [GeoDistance] +extension DoubleToGeoDistance on num { + /// Create a distance from radians + GeoDistance get radians => GeoDistance(toDouble()); + + /// Create a distance from degrees + GeoDistance get degrees => GeoDistance.fromDegrees(toDouble()); + + /// Create a distance from meters + GeoDistance get meters => GeoDistance.fromMeters(toDouble()); + + /// Create a distance from kilometers + GeoDistance get kilometers => GeoDistance.fromKilometers(toDouble()); + + /// Create a distance from miles + GeoDistance get miles => GeoDistance.fromMiles(toDouble()); +} + +/// A circle on the earth's surface. +/// +/// This type can be used as the query argument for a `geoWithin` query. +/// It cannot be persisted as a property on a realm object. +final class GeoCircle implements GeoShape { + final GeoPoint center; + final GeoDistance radius; + + /// Create a circle from a [center] point and a [radius] + const GeoCircle(this.center, this.radius); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! GeoCircle) return false; + return center == other.center && radius == other.radius; + } + + @override + int get hashCode => Object.hash(center, radius); + + @override + String toString() => 'geoCircle($center, $radius)'; +} diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 7f97f8fd17..4752f99a50 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -2999,7 +2999,15 @@ void _intoRealmQueryArg(Object? value, Pointer realm_query_ar realm_query_arg.ref.arg = allocator(); realm_query_arg.ref.nb_args = 1; realm_query_arg.ref.is_list = false; - _intoRealmValue(value, realm_query_arg.ref.arg.ref, allocator); + _intoRealmValueHack(value, realm_query_arg.ref.arg.ref, allocator); + } +} + +void _intoRealmValueHack(Object? value, realm_value realm_value, Allocator allocator) { + if (value is GeoShape) { + _intoRealmValue(value.toString(), realm_value, allocator); + } else { + _intoRealmValue(value, realm_value, allocator); } } diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index da4b5c508d..0411b837dc 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -39,13 +39,19 @@ export 'package:cancellation_token/cancellation_token.dart' show CancellationTok export 'package:realm_common/realm_common.dart' show Backlink, + DoubleToGeoDistance, + GeoBox, + GeoCircle, + GeoDistance, + GeoPoint, + GeoRing, + GeoShape, Ignored, Indexed, MapTo, ObjectId, ObjectType, PrimaryKey, - RealmValue, RealmClosedError, RealmCollectionType, RealmError, @@ -54,6 +60,7 @@ export 'package:realm_common/realm_common.dart' RealmPropertyType, RealmStateError, RealmUnsupportedSetError, + RealmValue, Uuid; // always expose with `show` to explicitly control the public API surface diff --git a/test/geospatial_test.dart b/test/geospatial_test.dart new file mode 100644 index 0000000000..9d207e7ec8 --- /dev/null +++ b/test/geospatial_test.dart @@ -0,0 +1,366 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'dart:async'; +import 'dart:math'; + +import 'package:realm_common/realm_common.dart'; +import 'package:test/test.dart' hide test, throws; + +import '../lib/realm.dart'; +import 'test.dart'; + +part 'geospatial_test.g.dart'; + +@RealmModel(ObjectType.embeddedObject) +class _Location { + final String type = 'Point'; + final List coordinates = const [0, 0]; + + double get lon => coordinates[0]; + set lon(double value) => coordinates[0] = value; + + double get lat => coordinates[1]; + set lat(double value) => coordinates[1] = value; + + GeoPoint toGeoPoint() => GeoPoint(lon: lon, lat: lat); + + @override + toString() => '($lon, $lat)'; +} + +@RealmModel() +class _Restaurant { + @PrimaryKey() + late String name; + _Location? location; + + @override + String toString() => name; +} + +void createRestaurants(Realm realm) { + realm.write(() { + realm.add(Restaurant('Burger King', location: (0.0, 0.0).toLocation())); + }); +} + +@RealmModel() +class _LocationList { + final locations = <_Location>[]; + + @override + String toString() => '[${locations.join(', ')}]'; +} + +extension on GeoPoint { + Location toLocation() { + return Location(coordinates: [lon, lat]); + } +} + +extension on (num, num) { + (num, num) get r => ($2, $1); + GeoPoint toGeoPoint() => GeoPoint(lon: $1.toDouble(), lat: $2.toDouble()); + Location toLocation() => toGeoPoint().toLocation(); +} + +GeoRing ring(Iterable<(num, num)> coords, {bool close = true}) => GeoRing.from(coords.followedBy(close ? [coords.first] : []).map((c) => c.toGeoPoint())); + +Future main([List? args]) async { + await setupTests(args); + + final noma = Restaurant('Noma', location: (12.610534422524335, 55.682837071136916).toLocation()); + final theFatDuck = Restaurant('The Fat Duck', location: (-0.7017480029998424, 51.508054146883474).toLocation()); + final mugaritz = Restaurant('Mugaritz', location: (-1.9169972753911122, 43.27291163851115).toLocation()); + + final realm = Realm(Configuration.inMemory([Location.schema, Restaurant.schema])); + realm.write(() => realm.addAll([noma, theFatDuck, mugaritz])); + + final ringAroundNoma = ring([ + (12.7, 55.7), + (12.6, 55.7), + (12.6, 55.6), + ]); + final ringAroundTheFatDuck = ring([ + (-0.7, 51.6), + (-0.7, 51.5), + (-0.8, 51.5), + ]); + final ringAroundMugaritz = ring([ + (-2.0, 43.3), + (-1.9, 43.3), + (-1.9, 43.2), + ]); + // https://earth.google.com/earth/d/1yxJunwmJ8bOHVveoJZ_ProcCVO_VqYaz?usp=sharing + final ringAroundAll = ring([ + (14.28516468673617, 56.65398894416146), + (6.939337946654436, 56.27411809280813), + (-1.988816029211967, 52.42582816187998), + (-6.148020252081531, 49.09018453730319), + (-6.397402529198644, 42.92138665921894), + (-1.91041351386849, 41.49883413234565), + (1.196195056765141, 48.58429125105875), + (7.132994722232744, 52.7379048959241), + (11.0454979022267, 54.51275033599874), + ]); + + for (final (shape, restaurants) in [ + (GeoCircle(noma.location!.toGeoPoint(), 0.meters), [noma]), + (GeoCircle(theFatDuck.location!.toGeoPoint(), 10.meters), [theFatDuck]), + (GeoCircle(mugaritz.location!.toGeoPoint(), 10.meters), [mugaritz]), + (GeoCircle(noma.location!.toGeoPoint(), 1000.kilometers), [noma, theFatDuck]), + (GeoCircle(noma.location!.toGeoPoint(), 3000.miles), [noma, theFatDuck, mugaritz]), + (GeoBox((12.6, 55.6).toGeoPoint(), (12.7, 55.7).toGeoPoint()), [noma]), + (GeoBox((-0.8, 51.5).toGeoPoint(), (-0.7, 51.6).toGeoPoint()), [theFatDuck]), + (GeoBox((-2.0, 43.2).toGeoPoint(), (-1.9, 43.3).toGeoPoint()), [mugaritz]), + (GeoBox((-0.8, 51.5).toGeoPoint(), (12.7, 55.7).toGeoPoint()), [noma, theFatDuck]), + (GeoBox((-2.0, 43.2).toGeoPoint(), (12.7, 55.7).toGeoPoint()), [noma, theFatDuck, mugaritz]), + (GeoPolygon(ringAroundNoma), [noma]), + (GeoPolygon(ringAroundTheFatDuck), [theFatDuck]), + (GeoPolygon(ringAroundMugaritz), [mugaritz]), + ( + GeoPolygon([ + noma.location!.toGeoPoint(), + theFatDuck.location!.toGeoPoint(), + mugaritz.location!.toGeoPoint(), + noma.location!.toGeoPoint(), // close it + ]), + [], // corners not included (at least sometimes) + ), + ( + GeoPolygon(ringAroundAll), + [ + noma, + theFatDuck, + mugaritz, + ], + ), + ( + GeoPolygon(ringAroundAll, [ringAroundNoma]), + [ + theFatDuck, + mugaritz, + ], + ), + ( + GeoPolygon(ringAroundAll, [ringAroundNoma, ringAroundTheFatDuck]), + [ + mugaritz, + ], + ), + ( + GeoPolygon(ringAroundAll, [ringAroundNoma, ringAroundTheFatDuck, ringAroundMugaritz]), + [], + ) + ]) { + test('geo within $shape', () { + final results = realm.query('location geoWithin $shape'); + expect(results, unorderedEquals(restaurants)); + expect(results, realm.query('location geoWithin \$0', [shape])); + }); + } + + test('GeoPoint', () { + final p = (12, 42).toGeoPoint(); + final l = p.toLocation(); + + expect(p.lat, l.lat); + expect(p.lon, l.lon); + expect(l.coordinates, [p.lon, p.lat]); + }); + + test('GeoPoint', () { + final validPoints = <(double, double)>[ + (0.0, 0.0), + (double.minPositive, 0), + ]; + for (final (lat, lon) in validPoints) { + final p = (lon, lat).toGeoPoint(); + expect(p.lat, lat); + expect(p.lon, lon); + } + }); + + test('GeoPoint invalid args throws', () { + const latError = 'lat'; + const lonError = 'lon'; + final validPoints = <(double, double, String)>[ + (-90.1, 0, latError), + (double.negativeInfinity, 0, latError), + (90.1, 0, latError), + (double.infinity, 0, latError), + // lng violations + (0, -180.1, lonError), + (0, double.negativeInfinity, lonError), + (0, 180.1, lonError), + (0, double.infinity, lonError), + ]; + for (final (lat, lon, error) in validPoints) { + expect(() => GeoPoint(lat: lat, lon: lon), throwsA(isA().having((e) => e.name, 'name', contains(error)))); + } + }); + + test('GeoBox invalid args throws', () {}); + + test('GeoCircle invalid args throws', () {}); + + test('GeoPolygon invalid args throws', () { + expect(() => GeoPolygon(ring([(1, 1)])), throws('Ring must have at least 3 different')); + expect(() => GeoPolygon(ring([(1, 1), (2, 2)])), throws('Ring must have at least 3 different')); + expect(() => GeoPolygon(ring([(1, 1), (2, 2), (3, 3)], close: false)), throws('Vertices must form a ring (first != last)')); + expect(() => GeoPolygon(ring([(1, 1), (2, 2), (3, 3), (4, 4)], close: false)), throws('Vertices must form a ring (first != last)')); + }); + + test('GeoPoint.operator==', () { + final p = GeoPoint(lon: 1, lat: 1); + expect(p, equals(p)); + expect(p, equals((p as Object))); // ignore: unnecessary_cast + expect(p, equals(GeoPoint(lon: 1, lat: 1))); + expect(p, isNot(equals(GeoPoint(lon: 1, lat: 2)))); + expect(p, isNot(equals(Object()))); + }); + + test('GeoPoint.hashCode', () { + final p = GeoPoint(lon: 1, lat: 1); + expect(p.hashCode, p.hashCode); // stable + expect(p.hashCode, equals(GeoPoint(lon: 1, lat: 1).hashCode)); + expect(p.hashCode, isNot(equals(GeoPoint(lon: 1, lat: 2).hashCode))); + }); + + test('GeoPoint.toString', () { + final p = GeoPoint(lon: 1, lat: 1); + expect(p.toString(), '[1.0, 1.0]'); // we don't use WKT for some reason + }); + + test('GeoBox.operator==', () { + final b = GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 2)); + expect(b, equals(b)); + expect(b, equals((b as Object))); // ignore: unnecessary_cast + expect(b, equals(GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 2)))); + expect(b, isNot(equals(GeoBox(GeoPoint(lon: 1, lat: 2), GeoPoint(lon: 2, lat: 2))))); + expect(b, isNot(equals(GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 3))))); + expect(b, isNot(equals(Object()))); + }); + + test('GeoBox.hashCode', () { + final b = GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 2)); + expect(b.hashCode, b.hashCode); // stable + expect(b.hashCode, equals(GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 2)).hashCode)); + expect(b.hashCode, isNot(equals(GeoBox(GeoPoint(lon: 1, lat: 2), GeoPoint(lon: 2, lat: 2))).hashCode)); + expect(b.hashCode, isNot(equals(GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 3))).hashCode)); + }); + + test('GeoBox.toString', () { + final b = GeoBox(GeoPoint(lon: 1, lat: 1), GeoPoint(lon: 2, lat: 2)); + expect(b.toString(), 'geoBox([1.0, 1.0], [2.0, 2.0])'); // we don't use WKT for some reason + }); + + test('GeoCircle.operator==', () { + final c = GeoCircle(GeoPoint(lon: 1, lat: 1), 1.meters); + expect(c, equals(c)); + expect(c, equals((c as Object))); // ignore: unnecessary_cast + expect(c, equals(GeoCircle(GeoPoint(lon: 1, lat: 1), 1.meters))); + expect(c, isNot(equals(GeoCircle(GeoPoint(lon: 1, lat: 2), 1.meters)))); + expect(c, isNot(equals(GeoCircle(GeoPoint(lon: 1, lat: 1), 2.meters)))); + expect(c, isNot(equals(Object()))); + }); + + test('GeoCircle.hashCode', () { + final c = GeoCircle(GeoPoint(lon: 1, lat: 1), 1.meters); + expect(c.hashCode, c.hashCode); // stable + expect(c.hashCode, equals(GeoCircle(GeoPoint(lon: 1, lat: 1), 1.meters).hashCode)); + expect(c.hashCode, isNot(equals(GeoCircle(GeoPoint(lon: 1, lat: 2), 1.meters).hashCode))); + expect(c.hashCode, isNot(equals(GeoCircle(GeoPoint(lon: 1, lat: 1), 2.meters).hashCode))); + }); + + test('GeoCircle.toString', () { + final c = GeoCircle(GeoPoint(lon: 1, lat: 1), 1.meters); + expect(c.toString(), 'geoCircle([1.0, 1.0], ${1.meters.radians})'); // we don't use WKT for some reason + }); + + test('GeoPolygon.operator==', () { + final p = GeoPolygon(ring([(1, 1), (2, 2), (3, 3)])); + expect(p, equals(p)); + expect(p, equals((p as Object))); // ignore: unnecessary_cast + // for efficiency we don't check the ring + expect(p, isNot(equals(GeoPolygon(ring([(1, 1), (2, 2), (3, 3)]))))); + expect(p, isNot(equals(GeoPolygon(ring([(1, 1), (2, 2), (3, 4)]))))); + expect(p, isNot(equals(Object()))); + }); + + test('GeoPolygon.hashCode', () { + final p = GeoPolygon(ring([(1, 1), (2, 2), (3, 3)])); + expect(p.hashCode, p.hashCode); // stable + // for efficiency we don't check the ring + expect(p.hashCode, isNot(equals(GeoPolygon(ring([(1, 1), (2, 2), (3, 3)])).hashCode))); + expect(p.hashCode, isNot(equals(GeoPolygon(ring([(1, 1), (2, 2), (3, 4)])).hashCode))); + }); + + test('GeoPolygon.toString', () { + final p = GeoPolygon(ring([(1, 1), (2, 2), (3, 3)])); + expect(p.toString(), 'geoPolygon({[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [1.0, 1.0]})'); // we don't use WKT for some reason + }); + + test('GeoDistance', () { + final d = GeoDistance(1); + expect(d.degrees, 180 / pi); + expect(d.meters, 1 / 1.5678502891116e-7); + expect(d.kilometers, 1 / 1.5678502891116e-7 / 1000); + expect(d.miles, d.meters / 1609.344); + expect(d.toString(), '1.0'); + + expect(1.radians, const GeoDistance(1)); + expect(1.degrees, GeoDistance.fromDegrees(1)); + expect(1.meters, GeoDistance.fromMeters(1)); + expect(1.kilometers, GeoDistance.fromKilometers(1)); + expect(1.miles, GeoDistance.fromMiles(1)); + + expect(1000.meters, 1.kilometers); + expect(1609.344.meters, 1.miles); + }); + + test('LocationList', () { + final config = Configuration.local([Location.schema, LocationList.schema]); + final realm = getRealm(config); + const max = 3; + final random = Random(42); + final geoPoints = List.generate(max, (index) => GeoPoint(lon: random.nextDouble(), lat: random.nextDouble())); + final sublists = List.generate(max, (index) { + final start = random.nextInt(max); + final end = random.nextInt(max - start) + start; + return geoPoints.sublist(start, end).map((p) => p.toLocation()).toList(); + }); + realm.write(() { + realm.addAll(sublists.map((l) => LocationList(locations: l))); + }); + + for (final p in geoPoints) { + final results = realm.query('ANY locations geoWithin \$0', [GeoCircle(p, 0.radians)]); + for (final list in results) { + expect(list.locations.map((l) => l.toGeoPoint()), contains(p)); + } + } + final nonEmpty = realm.query('locations.@size > 0'); + final bigCircle = realm.query('ALL locations geoWithin \$0', [GeoCircle(GeoPoint(lon: 0, lat: 0), sqrt(2).radians)]); + expect(bigCircle, unorderedEquals(nonEmpty)); + + final box = GeoBox(GeoPoint(lon: 0, lat: 0), GeoPoint(lon: 1, lat: 1)); + expect(realm.query('ALL locations geoWithin \$0', [box]), unorderedEquals(nonEmpty)); + }); +} diff --git a/test/geospatial_test.g.dart b/test/geospatial_test.g.dart new file mode 100644 index 0000000000..dfad1d134a --- /dev/null +++ b/test/geospatial_test.g.dart @@ -0,0 +1,140 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'geospatial_test.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class Location extends _Location + with RealmEntity, RealmObjectBase, EmbeddedObject { + static var _defaultsSet = false; + + Location({ + String type = 'Point', + Iterable coordinates = const [], + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'type': 'Point', + }); + } + RealmObjectBase.set(this, 'type', type); + RealmObjectBase.set>( + this, 'coordinates', RealmList(coordinates)); + } + + Location._(); + + @override + String get type => RealmObjectBase.get(this, 'type') as String; + + @override + RealmList get coordinates => + RealmObjectBase.get(this, 'coordinates') as RealmList; + @override + set coordinates(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Location freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Location._); + return const SchemaObject(ObjectType.embeddedObject, Location, 'Location', [ + SchemaProperty('type', RealmPropertyType.string), + SchemaProperty('coordinates', RealmPropertyType.double, + collectionType: RealmCollectionType.list), + ]); + } +} + +// ignore_for_file: type=lint +class Restaurant extends _Restaurant + with RealmEntity, RealmObjectBase, RealmObject { + Restaurant( + String name, { + Location? location, + }) { + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'location', location); + } + + Restaurant._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + Location? get location => + RealmObjectBase.get(this, 'location') as Location?; + @override + set location(covariant Location? value) => + RealmObjectBase.set(this, 'location', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Restaurant freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Restaurant._); + return const SchemaObject( + ObjectType.realmObject, Restaurant, 'Restaurant', [ + SchemaProperty('name', RealmPropertyType.string, primaryKey: true), + SchemaProperty('location', RealmPropertyType.object, + optional: true, linkTarget: 'Location'), + ]); + } +} + +// ignore_for_file: type=lint +class LocationList extends _LocationList + with RealmEntity, RealmObjectBase, RealmObject { + LocationList({ + Iterable locations = const [], + }) { + RealmObjectBase.set>( + this, 'locations', RealmList(locations)); + } + + LocationList._(); + + @override + RealmList get locations => + RealmObjectBase.get(this, 'locations') as RealmList; + @override + set locations(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + LocationList freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(LocationList._); + return const SchemaObject( + ObjectType.realmObject, LocationList, 'LocationList', [ + SchemaProperty('locations', RealmPropertyType.object, + linkTarget: 'Location', collectionType: RealmCollectionType.list), + ]); + } +} From 27742d2b00dfd24584c1528609a497b39c732a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 1 Nov 2023 14:19:30 +0100 Subject: [PATCH 06/16] asymmetric object allow non embedded links (#1402) Co-authored-by: Nikola Irinchev --- CHANGELOG.md | 3 ++- generator/lib/src/class_element_ex.dart | 8 +------- .../asymmetric_link_to_non_embedded.dart | 13 ------------- .../asymmetric_link_to_non_embedded.expected | 11 ----------- .../asymmetric_link_to_non_embedded_list.dart | 13 ------------- .../asymmetric_link_to_non_embedded_list.expected | 11 ----------- test/asymmetric_test.dart | 5 ++--- test/test.dart | 1 + test/test.g.dart | 11 +++++++++++ 9 files changed, 17 insertions(+), 59 deletions(-) delete mode 100644 generator/test/error_test_data/asymmetric_link_to_non_embedded.dart delete mode 100644 generator/test/error_test_data/asymmetric_link_to_non_embedded.expected delete mode 100644 generator/test/error_test_data/asymmetric_link_to_non_embedded_list.dart delete mode 100644 generator/test/error_test_data/asymmetric_link_to_non_embedded_list.expected diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb4c3d96d..a840833c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * `SyncClientError`, `SyncConnectionError`, `SyncSessionError`, `SyncWebSocketError`, `GeneralSyncError` - replaced by `SyncError`. * `SyncClientErrorCode`, `SyncConnectionErrorCode`, `SyncSessionErrorCode`, `SyncWebSocketErrorCode`, `GeneralSyncErrorCode, SyncErrorCategory` - replaced by `SyncErrorCode`. * Throw an exception if `File::unlock` has failed, in order to inform the SDK that we are likely hitting some limitation on the OS filesystem, instead of crashing the application and use the same file locking logic for all the platforms. (Core upgrade) +* Lift a restriction that prevents asymmetric objects from linking to non-embedded objects. ([#1403](https://github.com/realm/realm-dart/issues/1403)) ### Fixed * Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) @@ -31,7 +32,7 @@ * Sync protocol version bumped to 10. (Core upgrade) * Handle `badChangeset` error when printing changeset contents in debug. (Core upgrade) -* Using Core 13.20.1. +* Using Core 13.23.2. ## 1.5.0 (2023-09-18) diff --git a/generator/lib/src/class_element_ex.dart b/generator/lib/src/class_element_ex.dart index 3305fb3c13..60d40192f7 100644 --- a/generator/lib/src/class_element_ex.dart +++ b/generator/lib/src/class_element_ex.dart @@ -188,17 +188,11 @@ extension ClassElementEx on ClassElement { } } - // Check that asymmetric objects: - // 1) only have links to embedded objects. - // 2) have a primary key named _id. + // Check that asymmetric objects have a primary key named _id. if (objectType == ObjectType.asymmetricObject) { var hasPrimaryKey = false; for (final field in mappedFields) { final fieldElement = field.fieldElement; - final classElement = fieldElement.type.basicType.element as ClassElement; - if (field.type.basicType.isRealmModel && !classElement.thisType.isRealmModelOfType(ObjectType.embeddedObject)) { - throw RealmInvalidGenerationSourceError('Asymmetric objects cannot link to non-embedded objects', todo: '', element: fieldElement); - } if (field.isPrimaryKey) { hasPrimaryKey = true; if (field.realmName != '_id') { diff --git a/generator/test/error_test_data/asymmetric_link_to_non_embedded.dart b/generator/test/error_test_data/asymmetric_link_to_non_embedded.dart deleted file mode 100644 index 5d591cf8b5..0000000000 --- a/generator/test/error_test_data/asymmetric_link_to_non_embedded.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:realm_common/realm_common.dart'; - -@RealmModel() -class _Symmetric {} - -@RealmModel(ObjectType.asymmetricObject) -class _Asymmetric { - @PrimaryKey() - @MapTo('_id') - late ObjectId id; - - _Symmetric? illegal; -} diff --git a/generator/test/error_test_data/asymmetric_link_to_non_embedded.expected b/generator/test/error_test_data/asymmetric_link_to_non_embedded.expected deleted file mode 100644 index 254882d74f..0000000000 --- a/generator/test/error_test_data/asymmetric_link_to_non_embedded.expected +++ /dev/null @@ -1,11 +0,0 @@ -Asymmetric objects cannot link to non-embedded objects - -in: asset:pkg/test/error_test_data/asymmetric_link_to_non_embedded.dart:12:15 - ╷ -6 │ @RealmModel(ObjectType.asymmetricObject) -7 │ class _Asymmetric { - │ ━━━━━━━━━━━ in realm model for 'Asymmetric' -... │ -12 │ _Symmetric? illegal; - │ ^^^^^^^ ! - ╵ diff --git a/generator/test/error_test_data/asymmetric_link_to_non_embedded_list.dart b/generator/test/error_test_data/asymmetric_link_to_non_embedded_list.dart deleted file mode 100644 index f71820ebbf..0000000000 --- a/generator/test/error_test_data/asymmetric_link_to_non_embedded_list.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:realm_common/realm_common.dart'; - -@RealmModel() -class _Symmetric {} - -@RealmModel(ObjectType.asymmetricObject) -class _Asymmetric { - @PrimaryKey() - @MapTo('_id') - late ObjectId id; - - late List<_Symmetric> illegal; -} diff --git a/generator/test/error_test_data/asymmetric_link_to_non_embedded_list.expected b/generator/test/error_test_data/asymmetric_link_to_non_embedded_list.expected deleted file mode 100644 index e6cce23f0c..0000000000 --- a/generator/test/error_test_data/asymmetric_link_to_non_embedded_list.expected +++ /dev/null @@ -1,11 +0,0 @@ -Asymmetric objects cannot link to non-embedded objects - -in: asset:pkg/test/error_test_data/asymmetric_link_to_non_embedded_list.dart:12:25 - ╷ -6 │ @RealmModel(ObjectType.asymmetricObject) -7 │ class _Asymmetric { - │ ━━━━━━━━━━━ in realm model for 'Asymmetric' -... │ -12 │ late List<_Symmetric> illegal; - │ ^^^^^^^ ! - ╵ diff --git a/test/asymmetric_test.dart b/test/asymmetric_test.dart index a7584b9442..00fb6cd463 100644 --- a/test/asymmetric_test.dart +++ b/test/asymmetric_test.dart @@ -59,7 +59,7 @@ Future main([List? args]) async { await realm.syncSession.waitForUpload(); }); - baasTest('Asymmetric tricks to add non-embedded links', (config) async { + baasTest('Asymmetric add non-embedded links', (config) async { final realm = await getIntegrationRealm(appConfig: config); realm.subscriptions.update((mutableSubscriptions) { @@ -68,8 +68,7 @@ Future main([List? args]) async { realm.write(() { final s = realm.add(Symmetric(ObjectId())); - // Since this shenanigan is allowed, I have opened a feature request to allow - // direct links on a symmetric objects. See https://github.com/realm/realm-core/issues/6976. + realm.ingest(Asymmetric(ObjectId(), symmetric: s)); realm.ingest(Asymmetric(ObjectId(), embeddedObjects: [Embedded(1, symmetric: s)])); realm.ingest(Asymmetric(ObjectId(), embeddedObjects: [Embedded(1, any: RealmValue.from(s))])); }); diff --git a/test/test.dart b/test/test.dart index 8f16db5767..2df22d03a5 100644 --- a/test/test.dart +++ b/test/test.dart @@ -333,6 +333,7 @@ class _Asymmetric { @MapTo('_id') late ObjectId id; + _Symmetric? symmetric; late List<_Embedded> embeddedObjects; } diff --git a/test/test.g.dart b/test/test.g.dart index 3f7e00d55b..dce0ed1f41 100644 --- a/test/test.g.dart +++ b/test/test.g.dart @@ -2098,9 +2098,11 @@ class Asymmetric extends _Asymmetric with RealmEntity, RealmObjectBase, AsymmetricObject { Asymmetric( ObjectId id, { + Symmetric? symmetric, Iterable embeddedObjects = const [], }) { RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'symmetric', symmetric); RealmObjectBase.set>( this, 'embeddedObjects', RealmList(embeddedObjects)); } @@ -2112,6 +2114,13 @@ class Asymmetric extends _Asymmetric @override set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + @override + Symmetric? get symmetric => + RealmObjectBase.get(this, 'symmetric') as Symmetric?; + @override + set symmetric(covariant Symmetric? value) => + RealmObjectBase.set(this, 'symmetric', value); + @override RealmList get embeddedObjects => RealmObjectBase.get(this, 'embeddedObjects') @@ -2135,6 +2144,8 @@ class Asymmetric extends _Asymmetric ObjectType.asymmetricObject, Asymmetric, 'Asymmetric', [ SchemaProperty('id', RealmPropertyType.objectid, mapTo: '_id', primaryKey: true), + SchemaProperty('symmetric', RealmPropertyType.object, + optional: true, linkTarget: 'Symmetric'), SchemaProperty('embeddedObjects', RealmPropertyType.object, linkTarget: 'Embedded', collectionType: RealmCollectionType.list), ]); From 8792ffffc9b9a53f4534a326d33cc3da94c1ed15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 9 Nov 2023 23:05:32 +0100 Subject: [PATCH 07/16] kn/add certificate (#1378) Co-authored-by: Nikola Irinchev --- CHANGELOG.md | 2 ++ lib/src/app.dart | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a840833c5d..e54013b596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * `SyncClientErrorCode`, `SyncConnectionErrorCode`, `SyncSessionErrorCode`, `SyncWebSocketErrorCode`, `GeneralSyncErrorCode, SyncErrorCategory` - replaced by `SyncErrorCode`. * Throw an exception if `File::unlock` has failed, in order to inform the SDK that we are likely hitting some limitation on the OS filesystem, instead of crashing the application and use the same file locking logic for all the platforms. (Core upgrade) * Lift a restriction that prevents asymmetric objects from linking to non-embedded objects. ([#1403](https://github.com/realm/realm-dart/issues/1403)) +* Add ISRG X1 Root certificate (used by lets-encrypt and hence MongoDB) to `SecurityContext` of the default `HttpClient`. This ensure we work out-of-the-box on older devices (in particular Android 7 and earlier), as well as some Windows machines. ([#1187](https://github.com/realm/realm-dart/issues/1187), [#1370](https://github.com/realm/realm-dart/issues/1370)) ### Fixed * Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) @@ -66,6 +67,7 @@ * Fix failed assertion for unknown app server errors (Core upgrade, since v12.9.0). * Testing the size of a collection of links against zero would sometimes fail (sometimes = "difficult to explain"). (Core upgrade, since v13.15.1) * `Session.getProgressStream` now returns a regular stream, instead of a broadcast stream. ([#1375](https://github.com/realm/realm-dart/pull/1375)) +* Add ISRG X1 Root certificate (used by lets-encrypt and hence MongoDB) to `SecurityContext` of the default `HttpClient`. This ensure we work out-of-the-box on older devices (in particular Android 7 and earlier), as well as some Windows machines. ([#1187](https://github.com/realm/realm-dart/issues/1187), [#1370](https://github.com/realm/realm-dart/issues/1370)) ### Compatibility * Realm Studio: 13.0.0 or later. diff --git a/lib/src/app.dart b/lib/src/app.dart index 298234efd7..4116c1c34e 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -15,6 +15,7 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// +import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; @@ -26,6 +27,48 @@ import 'credentials.dart'; import 'native/realm_core.dart'; import 'user.dart'; +final _defaultClient = () { + const isrgRootX1CertPEM = // The root certificate used by lets encrypt and hence MongoDB + ''' +subject=CN=ISRG Root X1,O=Internet Security Research Group,C=US +issuer=CN=DST Root CA X3,O=Digital Signature Trust Co. +-----BEGIN CERTIFICATE----- +MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC +ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL +wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D +LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK +4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 +bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y +sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ +Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 +FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc +SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql +PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND +TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 +c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx ++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB +ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu +b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E +U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu +MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC +5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW +9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG +WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O +he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC +Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 +-----END CERTIFICATE-----'''; + + final context = SecurityContext(withTrustedRoots: true); + context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM)); + return HttpClient(context: context); +}(); + /// A class exposing configuration options for an [App] /// {@category Application} @immutable @@ -102,7 +145,7 @@ class AppConfiguration { HttpClient? httpClient, }) : baseUrl = baseUrl ?? Uri.parse('https://realm.mongodb.com'), baseFilePath = baseFilePath ?? Directory(_path.dirname(Configuration.defaultRealmPath)), - httpClient = httpClient ?? HttpClient(); + httpClient = httpClient ?? _defaultClient; } /// An [App] is the main client-side entry point for interacting with an [Atlas App Services](https://www.mongodb.com/docs/atlas/app-services/) application. From 581ceae70d3e3a768cb45e24a55ab87a3c7ca546 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 10 Nov 2023 09:57:28 +0100 Subject: [PATCH 08/16] Handle duplicate certificates (#1425) --- lib/src/app.dart | 15 ++++++++++++--- topic.md | 0 2 files changed, 12 insertions(+), 3 deletions(-) delete mode 100644 topic.md diff --git a/lib/src/app.dart b/lib/src/app.dart index 4116c1c34e..90d499f329 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -64,9 +64,18 @@ he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 -----END CERTIFICATE-----'''; - final context = SecurityContext(withTrustedRoots: true); - context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM)); - return HttpClient(context: context); + try { + final context = SecurityContext(withTrustedRoots: true); + context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM)); + return HttpClient(context: context); + } on TlsException catch (e) { + if (e.osError?.message.contains("CERT_ALREADY_IN_HASH_TABLE") == true) { + // certificate is already trusted. Nothing to do here + return HttpClient(); + } else { + rethrow; + } + } }(); /// A class exposing configuration options for an [App] diff --git a/topic.md b/topic.md deleted file mode 100644 index e69de29bb2..0000000000 From 6cb68222863cf828fd79dc490b7deb90f1fd9553 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 14 Nov 2023 16:46:30 +0100 Subject: [PATCH 09/16] Use default context rather than creating a new one (#1426) --- lib/src/app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 90d499f329..69ec3369a5 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -65,7 +65,7 @@ Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 -----END CERTIFICATE-----'''; try { - final context = SecurityContext(withTrustedRoots: true); + final context = SecurityContext.defaultContext; context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM)); return HttpClient(context: context); } on TlsException catch (e) { From b6eb23ba3c34d51827a70ad43cbc201145caa8a3 Mon Sep 17 00:00:00 2001 From: Desislava Stefanova <95419820+desistefanova@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:18:23 +0200 Subject: [PATCH 10/16] Flexible sync subscribe/unsubscribe API (#1354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kasper Overgård Nielsen Co-authored-by: Nikola Irinchev --- .github/workflows/ci.yml | 9 +- .github/workflows/dart-desktop-tests.yml | 6 +- .github/workflows/flutter-desktop-tests.yml | 4 + CHANGELOG.md | 3 +- lib/src/app.dart | 7 +- lib/src/native/realm_core.dart | 89 +++--- lib/src/realm_class.dart | 4 +- lib/src/results.dart | 118 +++++++- lib/src/session.dart | 4 +- lib/src/subscription.dart | 34 ++- test/realm_test.dart | 5 +- test/session_test.dart | 13 + test/subscription_test.dart | 294 +++++++++++++++++--- test/test.dart | 28 +- 14 files changed, 495 insertions(+), 123 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f97d840a76..b31de514b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -673,24 +673,23 @@ jobs: slack-on-failure: name: Report failure in main branch - needs: + needs: - cleanup-dart-matrix - cleanup-flutter-matrix runs-on: ubuntu-latest - if: always() + if: always() && github.ref == 'refs/heads/main' steps: # Run this action to set env.WORKFLOW_CONCLUSION - uses: technote-space/workflow-conclusion-action@45ce8e0eb155657ab8ccf346ade734257fd196a5 - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f - if: ${{ github.ref == 'refs/heads/main' && env.WORKFLOW_CONCLUSION == 'FAILURE' }} + if: ${{ github.ref == 'refs/heads/main' && env.WORKFLOW_CONCLUSION == 'FAILURE' }} # Statuses: neutral, success, skipped, cancelled, timed_out, action_required, failure with: - status: ${{ env.WORKFLOW_CONCLUSION }} + status: ${{ env.WORKFLOW_CONCLUSION }} webhook-url: ${{ secrets.SLACK_DART_WEBHOOK }} channel: '#realm-github-dart' message: | ** <{{refUrl}}|`{{ref}}` - {{description}}> {{#if description}}<{{diffUrl}}|branch: `{{diffRef}}`>{{/if}} - \ No newline at end of file diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index b3082b7165..72a7fe3a29 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -58,12 +58,16 @@ jobs: - name : Setup Dart SDK uses: dart-lang/setup-dart@main with: - sdk: ${{ contains(inputs.os, 'linux') && '3.0.6' || 'stable'}} + sdk: 'stable' architecture: ${{ inputs.architecture == 'arm' && 'arm64' || 'x64'}} - name: Install dependencies run: dart pub get + - name: Bump ulimit + run: ulimit -n 10240 + if: ${{ contains(inputs.os, 'macos') }} + # This will be a no-op under normal circumstances since the cluster would have been deployed # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - name: Create cluster diff --git a/.github/workflows/flutter-desktop-tests.yml b/.github/workflows/flutter-desktop-tests.yml index 0650c5ccf3..8f7568f60c 100644 --- a/.github/workflows/flutter-desktop-tests.yml +++ b/.github/workflows/flutter-desktop-tests.yml @@ -74,6 +74,10 @@ jobs: - name: Install dependencies run: dart pub get + - name: Bump ulimit + run: ulimit -n 10240 + if: ${{ contains(inputs.os, 'macos') }} + # This will be a no-op under normal circumstances since the cluster would have been deployed # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - name: Create cluster diff --git a/CHANGELOG.md b/CHANGELOG.md index e54013b596..7ac28006b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ * Throw an exception if `File::unlock` has failed, in order to inform the SDK that we are likely hitting some limitation on the OS filesystem, instead of crashing the application and use the same file locking logic for all the platforms. (Core upgrade) * Lift a restriction that prevents asymmetric objects from linking to non-embedded objects. ([#1403](https://github.com/realm/realm-dart/issues/1403)) * Add ISRG X1 Root certificate (used by lets-encrypt and hence MongoDB) to `SecurityContext` of the default `HttpClient`. This ensure we work out-of-the-box on older devices (in particular Android 7 and earlier), as well as some Windows machines. ([#1187](https://github.com/realm/realm-dart/issues/1187), [#1370](https://github.com/realm/realm-dart/issues/1370)) +* Added new flexible sync API `RealmResults.subscribe()` and `RealmResults.unsubscribe()` as an easy way to create subscriptions and download data in background. Added named parameter to `MutableSubscriptionSet.clear({bool unnamedOnly = false})` for removing all the unnamed subscriptions. ([#1354](https://github.com/realm/realm-dart/pull/1354)) +* Added `cancellationToken` parameter to `Session.waitForDownload()`, `Session.waitForUpload()` and `SubscriptionSet.waitForSynchronization()`. ([#1354](https://github.com/realm/realm-dart/pull/1354)) ### Fixed * Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409)) @@ -55,7 +57,6 @@ * Added support for query on `RealmSet`. ([#1346](https://github.com/realm/realm-dart/pull/1346)) * Support for passing `List`, `Set` or `Iterable` arguments to queries with `IN`-operators. ([#1346](https://github.com/realm/realm-dart/pull/1346)) - ### Fixed * Fixed an early unlock race condition during client reset callbacks. ([#1335](https://github.com/realm/realm-dart/pull/1335)) * Rare corruption of files on streaming format (often following compact, convert or copying to a new file). (Core upgrade, since v12.12.0) diff --git a/lib/src/app.dart b/lib/src/app.dart index 69ec3369a5..95b359a239 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -266,6 +266,11 @@ class AppException extends RealmException { @override String toString() { - return "AppException: $message, link to server logs: $linkToServerLogs"; + var errorString = "AppException: $message, status code: $statusCode"; + if (linkToServerLogs != null) { + errorString += ", link to server logs: $linkToServerLogs"; + } + + return errorString; } } diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 4752f99a50..5f02efb8c0 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -413,17 +413,21 @@ class _RealmCore { } static void _stateChangeCallback(Object userdata, int state) { - final completer = userdata as Completer; - - completer.complete(SubscriptionSetState.values[state]); + final completer = userdata as CancellableCompleter; + if (!completer.isCancelled) { + completer.complete(SubscriptionSetState.values[state]); + } } - Future waitForSubscriptionSetStateChange(SubscriptionSet subscriptions, SubscriptionSetState notifyWhen) { - final completer = Completer(); - final callback = Pointer.fromFunction(_stateChangeCallback); - final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); - _realmLib.realm_sync_on_subscription_set_state_change_async(subscriptions.handle._pointer, notifyWhen.index, - _realmLib.addresses.realm_dart_sync_on_subscription_state_changed_callback, userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); + Future waitForSubscriptionSetStateChange(SubscriptionSet subscriptions, SubscriptionSetState notifyWhen, + [CancellationToken? cancellationToken]) { + final completer = CancellableCompleter(cancellationToken); + if (!completer.isCancelled) { + final callback = Pointer.fromFunction(_stateChangeCallback); + final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); + _realmLib.realm_sync_on_subscription_set_state_change_async(subscriptions.handle._pointer, notifyWhen.index, + _realmLib.addresses.realm_dart_sync_on_subscription_state_changed_callback, userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); + } return completer.future; } @@ -666,23 +670,26 @@ class _RealmCore { Future openRealmAsync(RealmAsyncOpenTaskHandle handle, CancellationToken? cancellationToken) { final completer = CancellableCompleter(cancellationToken); - final callback = - Pointer.fromFunction realm, Pointer error)>(_openRealmAsyncCallback); - final userData = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); - _realmLib.realm_async_open_task_start( - handle._pointer, - _realmLib.addresses.realm_dart_async_open_task_callback, - userData.cast(), - _realmLib.addresses.realm_dart_userdata_async_free, - ); - + if (!completer.isCancelled) { + final callback = + Pointer.fromFunction realm, Pointer error)>(_openRealmAsyncCallback); + final userData = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); + _realmLib.realm_async_open_task_start( + handle._pointer, + _realmLib.addresses.realm_dart_async_open_task_callback, + userData.cast(), + _realmLib.addresses.realm_dart_userdata_async_free, + ); + } return completer.future; } static void _openRealmAsyncCallback(Object userData, Pointer realmSafePtr, Pointer error) { return using((Arena arena) { - final completer = userData as Completer; - + final completer = userData as CancellableCompleter; + if (completer.isCancelled) { + return; + } if (error != nullptr) { final err = arena(); bool success = _realmLib.realm_get_async_error(error, err); @@ -1755,13 +1762,12 @@ class _RealmCore { await using((arena) async { final response_pointer = arena(); final responseRef = response_pointer.ref; + final method = _HttpMethod.values[requestMethod]; + try { // Build request late HttpClientRequest request; - // this throws if requestMethod is unknown _HttpMethod - final method = _HttpMethod.values[requestMethod]; - switch (method) { case _HttpMethod.delete: request = await client.deleteUrl(url); @@ -1788,8 +1794,16 @@ class _RealmCore { request.add(utf8.encode(body)); } + Realm.logger.log(RealmLogLevel.debug, "HTTP Transport: Executing ${method.name} $url"); + + final stopwatch = Stopwatch()..start(); + // Do the call.. final response = await request.close(); + + stopwatch.stop(); + Realm.logger.log(RealmLogLevel.debug, "HTTP Transport: Executed ${method.name} $url: ${response.statusCode} in ${stopwatch.elapsedMilliseconds} ms"); + final responseBody = await response.fold>([], (acc, l) => acc..addAll(l)); // gather response // Report back to core @@ -1816,11 +1830,14 @@ class _RealmCore { }); responseRef.custom_status_code = _CustomErrorCode.noError.code; - } on SocketException catch (_) { + } on SocketException catch (socketEx) { + Realm.logger.log(RealmLogLevel.warn, "HTTP Transport: SocketException executing ${method.name} $url: $socketEx"); responseRef.custom_status_code = _CustomErrorCode.timeout.code; - } on HttpException catch (_) { + } on HttpException catch (httpEx) { + Realm.logger.log(RealmLogLevel.warn, "HTTP Transport: HttpException executing ${method.name} $url: $httpEx"); responseRef.custom_status_code = _CustomErrorCode.unknownHttp.code; - } catch (_) { + } catch (ex) { + Realm.logger.log(RealmLogLevel.error, "HTTP Transport: Exception executing ${method.name} $url: $ex"); responseRef.custom_status_code = _CustomErrorCode.unknown.code; } finally { _realmLib.realm_http_transport_complete_request(request_context, response_pointer); @@ -2320,12 +2337,14 @@ class _RealmCore { controller.onConnectionStateChange(ConnectionState.values[oldState], ConnectionState.values[newState]); } - Future sessionWaitForUpload(Session session) { - final completer = Completer(); - 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); + Future sessionWaitForUpload(Session session, [CancellationToken? cancellationToken]) { + final completer = CancellableCompleter(cancellationToken); + if (!completer.isCancelled) { + 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); + } return completer.future; } @@ -2341,8 +2360,8 @@ class _RealmCore { } static void _sessionWaitCompletionCallback(Object userdata, Pointer errorCode) { - final completer = userdata as Completer; - if (completer.isCompleted) { + final completer = userdata as CancellableCompleter; + if (completer.isCancelled) { return; } if (errorCode != nullptr) { diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 0411b837dc..1845cd5bc2 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -35,7 +35,7 @@ import 'session.dart'; import 'subscription.dart'; import 'set.dart'; -export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, CancelledException; +export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, TimeoutCancellationToken, CancelledException; export 'package:realm_common/realm_common.dart' show Backlink, @@ -122,7 +122,7 @@ export 'realm_object.dart' RealmObjectChanges, UserCallbackException; export 'realm_property.dart'; -export 'results.dart' show RealmResultsOfObject, RealmResultsChanges, RealmResults; +export 'results.dart' show RealmResultsOfObject, RealmResultsChanges, RealmResults, WaitForSyncMode, RealmResultsOfRealmObject; export 'session.dart' show ConnectionStateChange, diff --git a/lib/src/results.dart b/lib/src/results.dart index 160c49a927..84e53de46d 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -18,6 +18,8 @@ import 'dart:async'; import 'dart:ffi'; +import 'package:cancellation_token/cancellation_token.dart'; + import 'collections.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; @@ -162,6 +164,98 @@ extension RealmResultsOfObject on RealmResults { } } +class _SubscribedRealmResult extends RealmResults { + final String? subscriptionName; + + _SubscribedRealmResult._(RealmResults results, {this.subscriptionName}) + : super._( + results.handle, + results.realm, + results.metadata, + ); +} + +extension RealmResultsOfRealmObject on RealmResults { + /// Adds this [RealmResults] query to the set of active subscriptions. + /// The query will be joined via an OR statement with any existing queries for the same type. + /// + /// If a [name] is given this allows you to later refer to the subscription by name, + /// e.g. when calling [MutableSubscriptionSet.removeByName]. + /// + /// If [update] is specified to `true`, then any existing query + /// with the same name will be replaced. + /// Otherwise a [RealmException] is thrown, in case of duplicates. + /// + /// [WaitForSyncMode] specifies how to wait or not wait for subscribed objects to be downloaded. + /// The default value is [WaitForSyncMode.firstTime]. + /// + /// The [cancellationToken] is optional and can be used to cancel + /// the waiting for objects to be downloaded. + /// If the operation is cancelled, a [CancelledException] is thrown and the download + /// continues in the background. + /// In case of using [TimeoutCancellationToken] and the time limit is exceeded, + /// a [TimeoutException] is thrown and the download continues in the background. + /// + /// {@category Sync} + Future> subscribe({ + String? name, + WaitForSyncMode waitForSyncMode = WaitForSyncMode.firstTime, + CancellationToken? cancellationToken, + bool update = false, + }) async { + Subscription? existingSubscription = name == null ? realm.subscriptions.find(this) : realm.subscriptions.findByName(name); + late Subscription updatedSubscription; + realm.subscriptions.update((mutableSubscriptions) { + updatedSubscription = mutableSubscriptions.add(this, name: name, update: update); + }); + bool shouldWait = waitForSyncMode == WaitForSyncMode.always || + (waitForSyncMode == WaitForSyncMode.firstTime && subscriptionIsChanged(existingSubscription, updatedSubscription)); + + return await CancellableFuture.from>(() async { + if (cancellationToken != null && cancellationToken.isCancelled) { + throw cancellationToken.exception!; + } + if (shouldWait) { + await realm.subscriptions.waitForSynchronization(cancellationToken); + await realm.syncSession.waitForDownload(cancellationToken); + } + return _SubscribedRealmResult._(this, subscriptionName: name); + }, cancellationToken); + } + + /// Unsubscribe from this query result. It returns immediately + /// without waiting for synchronization. + /// + /// If the subscription is unnamed, the subscription matching + /// the query will be removed. + /// Return `false` if the [RealmResults] is not created by [subscribe]. + /// + /// {@category Sync} + bool unsubscribe() { + bool unsubscribed = false; + if (realm.config is! FlexibleSyncConfiguration) { + throw RealmError('unsubscribe is only allowed on Realms opened with a FlexibleSyncConfiguration'); + } + if (this is _SubscribedRealmResult) { + final subscriptionName = (this as _SubscribedRealmResult).subscriptionName; + realm.subscriptions.update((mutableSubscriptions) { + if (subscriptionName != null) { + unsubscribed = mutableSubscriptions.removeByName(subscriptionName); + } else { + unsubscribed = mutableSubscriptions.removeByQuery(this); + } + }); + } + return unsubscribed; + } + + bool subscriptionIsChanged(Subscription? existingSubscription, Subscription updatedSubscription) { + return existingSubscription == null || + existingSubscription.objectClassName != updatedSubscription.objectClassName || + existingSubscription.queryString != updatedSubscription.queryString; + } +} + /// @nodoc //RealmResults package internal members extension RealmResultsInternal on RealmResults { @@ -180,12 +274,7 @@ extension RealmResultsInternal on RealmResults { RealmObjectMetadata get metadata => _metadata!; - static RealmResults create( - RealmResultsHandle handle, - Realm realm, - RealmObjectMetadata? metadata, - [int skip = 0] - ) => + static RealmResults create(RealmResultsHandle handle, Realm realm, RealmObjectMetadata? metadata, [int skip = 0]) => RealmResults._(handle, realm, metadata, skip); } @@ -257,3 +346,20 @@ class _RealmResultsIterator implements Iterator { return true; } } + +/// +/// Behavior when waiting for subscribed objects to be synchronized/downloaded. +/// +enum WaitForSyncMode { + /// Waits until the objects have been downloaded from the server + /// the first time the subscription is created. If the subscription + /// already exists, the [RealmResults] is returned immediately. + firstTime, + + /// Always waits until the objects have been downloaded from the server. + always, + + /// Never waits for the download to complete, but keeps downloading the + /// objects in the background. + never, +} diff --git a/lib/src/session.dart b/lib/src/session.dart index 91c8fef07c..07058181ec 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -59,9 +59,11 @@ class Session implements Finalizable { void resume() => realmCore.sessionResume(this); /// Waits for the [Session] to finish all pending uploads. - Future waitForUpload() => realmCore.sessionWaitForUpload(this); + /// An optional [cancellationToken] can be used to cancel the wait operation. + Future waitForUpload([CancellationToken? cancellationToken]) => realmCore.sessionWaitForUpload(this, cancellationToken); /// Waits for the [Session] to finish all pending downloads. + /// An optional [cancellationToken] can be used to cancel the wait operation. Future waitForDownload([CancellationToken? cancellationToken]) => realmCore.sessionWaitForDownload(this, cancellationToken); /// Gets a [Stream] of [SyncProgress] that can be used to track upload or download progress. diff --git a/lib/src/subscription.dart b/lib/src/subscription.dart index 66fdea4c00..2f095c32e9 100644 --- a/lib/src/subscription.dart +++ b/lib/src/subscription.dart @@ -147,8 +147,8 @@ abstract class SubscriptionSet with IterableMixin implements Final return result == null ? null : Subscription._(result); } - Future _waitForStateChange(SubscriptionSetState state) async { - final result = await realmCore.waitForSubscriptionSetStateChange(this, state); + Future _waitForStateChange(SubscriptionSetState state, [CancellationToken? cancellationToken]) async { + final result = await realmCore.waitForSubscriptionSetStateChange(this, state, cancellationToken); realmCore.refreshSubscriptionSet(this); return result; } @@ -159,8 +159,9 @@ abstract class SubscriptionSet with IterableMixin implements Final /// the returned [Future] will complete immediately. If the state is /// [SubscriptionSetState.error], the returned future will throw an /// error. - Future waitForSynchronization() async { - final result = await _waitForStateChange(SubscriptionSetState.complete); + /// An optional [cancellationToken] can be used to cancel the wait operation. + Future waitForSynchronization([CancellationToken? cancellationToken]) async { + final result = await _waitForStateChange(SubscriptionSetState.complete, cancellationToken); if (result == SubscriptionSetState.error) { throw error!; } @@ -184,7 +185,7 @@ abstract class SubscriptionSet with IterableMixin implements Final @override Iterator get iterator => _SubscriptionIterator._(this); - /// Update the subscription set and send the request to the server in the background. + /// Updates the subscription set and send the request to the server in the background. /// /// Calling [update] is a prerequisite for mutating the subscription set, /// using a [MutableSubscriptionSet] passed to the [action]. @@ -272,21 +273,22 @@ class MutableSubscriptionSet extends SubscriptionSet { return Subscription._(realmCore.insertOrAssignSubscription(this, query, name, update)); } - /// Remove the [subscription] from the set, if it exists. + /// Removes the [subscription] from the set, if it exists. bool remove(Subscription subscription) { return realmCore.eraseSubscriptionById(this, subscription); } - /// Remove the [query] from the set, if it exists. + /// Removes the [query] from the set, if it exists. bool removeByQuery(RealmResults query) { return realmCore.eraseSubscriptionByResults(this, query); } - /// Remove the [query] from the set that matches by [name], if it exists. + /// Removes the subscription from the set that matches by [name], if it exists. bool removeByName(String name) { return realmCore.eraseSubscriptionByName(this, name); } + /// Removes the subscriptions from the set that matches by type, if it exists. bool removeByType() { final name = realm.schema.singleWhere((e) => e.type == T).name; var result = false; @@ -300,9 +302,19 @@ class MutableSubscriptionSet extends SubscriptionSet { return result; } - /// Clear the subscription set. - void clear() { - realmCore.clearSubscriptionSet(this); + /// Clears the subscription set. + /// If [unnamedOnly] is `true`, then only unnamed subscriptions will be removed. + void clear({bool unnamedOnly = false}) { + if (unnamedOnly) { + for (var i = length - 1; i >= 0; i--) { + final subscription = this[i]; + if (subscription.name == null) { + remove(subscription); + } + } + } else { + realmCore.clearSubscriptionSet(this); + } } } diff --git a/test/realm_test.dart b/test/realm_test.dart index ebd1845521..3315345fee 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -26,7 +26,6 @@ import 'package:test/test.dart' hide test, throws; import 'package:timezone/timezone.dart' as tz; 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 'test.dart'; import '../lib/src/native/realm_core.dart'; @@ -1971,8 +1970,8 @@ Future _addDataToAtlas(Realm realm, String productNamePrefix, {int itemsCo await realm.syncSession.waitForDownload(); } -Future _addSubscriptions(Realm realm, String searchByPreffix) async { - final query = realm.query(r'name BEGINSWITH $0', [searchByPreffix]); +Future _addSubscriptions(Realm realm, String searchByPrefix) async { + final query = realm.query(r'name BEGINSWITH $0', [searchByPrefix]); if (realm.subscriptions.find(query) == null) { realm.subscriptions.update((mutableSubscriptions) => mutableSubscriptions.add(query)); } diff --git a/test/session_test.dart b/test/session_test.dart index 2c60938a26..5423e2671b 100644 --- a/test/session_test.dart +++ b/test/session_test.dart @@ -135,6 +135,19 @@ Future main([List? args]) async { await realm.syncSession.waitForDownload(); }); + baasTest('SyncSession.waitForDownload/waitForUpload canceled', (configuration) async { + final realm = await getIntegrationRealm(); + final cancellationDownloadToken = CancellationToken(); + final waitForDownloadFuture = realm.syncSession.waitForDownload(cancellationDownloadToken); + cancellationDownloadToken.cancel(); + expect(() async => await waitForDownloadFuture, throwsA(isA())); + + final cancellationUploadToken = CancellationToken(); + final waitForUploadFuture = realm.syncSession.waitForUpload(cancellationUploadToken); + cancellationUploadToken.cancel(); + expect(() async => await waitForUploadFuture, throwsA(isA())); + }); + baasTest('SyncSesison.waitForUpload with changes', (configuration) async { final differentiator = ObjectId(); diff --git a/test/subscription_test.dart b/test/subscription_test.dart index 75cf7441bb..294cda083e 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -20,27 +20,13 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'package:meta/meta.dart'; import 'package:test/expect.dart' hide throws; import '../lib/realm.dart'; -import '../lib/src/configuration.dart'; import '../lib/src/native/realm_core.dart'; import '../lib/src/subscription.dart'; import 'test.dart'; -@isTest -void testSubscriptions(String name, FutureOr Function(Realm) testFunc) async { - baasTest(name, (appConfiguration) async { - final app = App(appConfiguration); - final credentials = Credentials.anonymous(); - final user = await app.logIn(credentials); - final configuration = Configuration.flexibleSync(user, getSyncSchema())..sessionStopPolicy = SessionStopPolicy.immediately; - final realm = getRealm(configuration); - await testFunc(realm); - }); -} - Future main([List? args]) async { await setupTests(args); @@ -50,13 +36,24 @@ Future main([List? args]) async { expect(() => realm.subscriptions, throws()); }); - testSubscriptions('SubscriptionSet.state/waitForSynchronization', (realm) async { + baasTest('SubscriptionSet.state/waitForSynchronization', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; await subscriptions.waitForSynchronization(); expect(subscriptions.state, SubscriptionSetState.complete); }); - testSubscriptions('SubscriptionSet.version', (realm) async { + baasTest('SubscriptionSet.state/waitForSynchronization canceled', (config) async { + final realm = await getIntegrationRealm(appConfig: config); + final subscriptions = realm.subscriptions; + final cancellationToken = CancellationToken(); + final waitFuture = subscriptions.waitForSynchronization(cancellationToken); + cancellationToken.cancel(); + expect(() async => await waitFuture, throwsA(isA())); + }); + + baasTest('SubscriptionSet.version', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; expect(subscriptions.version, 0); @@ -75,7 +72,8 @@ Future main([List? args]) async { expect(subscriptions.version, 2); }); - testSubscriptions('MutableSubscriptionSet.add', (realm) { + baasTest('MutableSubscriptionSet.add', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final query = realm.all(); @@ -86,7 +84,8 @@ Future main([List? args]) async { expect(subscriptions.find(query), isNotNull); }); - testSubscriptions('MutableSubscriptionSet.add named', (realm) { + baasTest('MutableSubscriptionSet.add named', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; const name = 'some name'; @@ -98,7 +97,8 @@ Future main([List? args]) async { expect(subscriptions.findByName(name), s); }); - testSubscriptions('SubscriptionSet.find', (realm) { + baasTest('SubscriptionSet.find', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final query = realm.all(); @@ -110,7 +110,8 @@ Future main([List? args]) async { expect(subscriptions.find(query), isNotNull); }); - testSubscriptions('SubscriptionSet.find return match, even if named', (realm) { + baasTest('SubscriptionSet.find return match, even if named', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final query = realm.all(); @@ -123,7 +124,8 @@ Future main([List? args]) async { expect(subscriptions.find(query), s); }); - testSubscriptions('SubscriptionSet.findByName', (realm) { + baasTest('SubscriptionSet.findByName', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; const name = 'some name'; @@ -135,7 +137,8 @@ Future main([List? args]) async { expect(subscriptions.findByName(name), isNotNull); }); - testSubscriptions('MutableSubscriptionSet.remove', (realm) { + baasTest('MutableSubscriptionSet.remove', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final query = realm.all(); @@ -152,7 +155,8 @@ Future main([List? args]) async { expect(subscriptions, isEmpty); }); - testSubscriptions('MutableSubscriptionSet.removeByQuery', (realm) { + baasTest('MutableSubscriptionSet.removeByQuery', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final query = realm.all(); @@ -167,7 +171,8 @@ Future main([List? args]) async { expect(subscriptions, isEmpty); }); - testSubscriptions('MutableSubscriptionSet.removeByName', (realm) { + baasTest('MutableSubscriptionSet.removeByName', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; const name = 'some name'; @@ -182,7 +187,8 @@ Future main([List? args]) async { expect(subscriptions, isEmpty); }); - testSubscriptions('MutableSubscriptionSet.removeAll', (realm) { + baasTest('MutableSubscriptionSet.removeAll', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; subscriptions.update((mutableSubscriptions) { @@ -197,7 +203,8 @@ Future main([List? args]) async { expect(subscriptions, isEmpty); }); - testSubscriptions('SubscriptionSet.elementAt', (realm) { + baasTest('SubscriptionSet.elementAt', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; subscriptions.update((mutableSubscriptions) { @@ -219,7 +226,8 @@ Future main([List? args]) async { expect(() => subscriptions[1000], throws()); }); - testSubscriptions('MutableSubscriptionSet.elementAt', (realm) { + baasTest('MutableSubscriptionSet.elementAt', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; subscriptions.update((mutableSubscriptions) { @@ -231,7 +239,8 @@ Future main([List? args]) async { }); }); - testSubscriptions('MutableSubscriptionSet.add double-add throws', (realm) { + baasTest('MutableSubscriptionSet.add double-add throws', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; // Adding same unnamed query twice without requesting an update will just de-duplicate @@ -258,7 +267,8 @@ Future main([List? args]) async { }, throws('Duplicate subscription')); }); - testSubscriptions('MutableSubscriptionSet.add with update flag', (realm) { + baasTest('MutableSubscriptionSet.add with update flag', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; subscriptions.update((mutableSubscriptions) { @@ -276,7 +286,8 @@ Future main([List? args]) async { expect(subscriptions.length, 2); }); - testSubscriptions('MutableSubscriptionSet.add multiple queries for same class', (realm) { + baasTest('MutableSubscriptionSet.add multiple queries for same class', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final random = Random.secure(); @@ -305,7 +316,8 @@ Future main([List? args]) async { } }); - testSubscriptions('MutableSubscriptionSet.add same name, different classes', (realm) { + baasTest('MutableSubscriptionSet.add same name, different classes', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; expect( @@ -316,7 +328,8 @@ Future main([List? args]) async { throws()); }); - testSubscriptions('MutableSubscriptionSet.add same name, different classes, with update flag', (realm) { + baasTest('MutableSubscriptionSet.add same name, different classes, with update flag', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; late Subscription subscription; @@ -329,7 +342,8 @@ Future main([List? args]) async { expect(subscriptions[0], subscription); // last added wins }); - testSubscriptions('MutableSubscriptionSet.add same query, different classes', (realm) { + baasTest('MutableSubscriptionSet.add same query, different classes', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; subscriptions.update((mutableSubscriptions) { @@ -343,7 +357,8 @@ Future main([List? args]) async { } }); - testSubscriptions('MutableSubscriptionSet.add illegal query', (realm) async { + baasTest('MutableSubscriptionSet.add illegal query', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; // Illegal query for subscription: @@ -356,7 +371,8 @@ Future main([List? args]) async { expect(() async => await subscriptions.waitForSynchronization(), throws("invalid RQL")); }); - testSubscriptions('MutableSubscriptionSet.remove same query, different classes', (realm) { + baasTest('MutableSubscriptionSet.remove same query, different classes', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; late Subscription s; @@ -374,7 +390,8 @@ Future main([List? args]) async { expect(subscriptions, [s]); }); - testSubscriptions('MutableSubscriptionSet.removeByType', (realm) { + baasTest('MutableSubscriptionSet.removeByType', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; late Subscription s; @@ -394,7 +411,8 @@ Future main([List? args]) async { expect(subscriptions, [s]); }); - testSubscriptions('Get subscriptions', (realm) async { + baasTest('Get subscriptions', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; expect(subscriptions, isEmpty); @@ -430,7 +448,8 @@ Future main([List? args]) async { await subscriptions.waitForSynchronization(); }); - testSubscriptions('Subscription properties roundtrip', (realm) async { + baasTest('Subscription properties roundtrip', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; final before = DateTime.now().toUtc(); @@ -507,7 +526,8 @@ Future main([List? args]) async { expect(() => realm.write(() => realm.add(Task(ObjectId()))), throws("no flexible sync subscription has been created")); }); - testSubscriptions('Filter realm data using query subscription', (realm) async { + baasTest('Filter realm data using query subscription', (config) async { + final realm = await getIntegrationRealm(appConfig: config); realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.add(realm.all()); }); @@ -537,12 +557,8 @@ Future main([List? args]) async { 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, getSyncSchema()); - final realm = getRealm(config); + baasTest('Subscriptions when realm is closed gets closed as well', (config) async { + final realm = await getIntegrationRealm(appConfig: config); final subscriptions = realm.subscriptions; expect(() => subscriptions.state, returnsNormally); @@ -578,4 +594,192 @@ Future main([List? args]) async { expect(writeReason.reason, 'write to ObjectID("$productId") in table "${writeReason.objectType}" not allowed; object is outside of the current query view'); expect(writeReason.primaryKey.value, productId); }); + + baasTest('Flexible sync subscribe/unsubscribe API', (config) async { + final realm = await getIntegrationRealm(appConfig: config); + final prefix = generateRandomString(4); + final byTestRun = "name BEGINSWITH '$prefix'"; + final query = realm.query(byTestRun); + await query.subscribe(); + + // Write new data and upload + realm.write(() { + realm.addAll([ + Event(ObjectId(), name: "$prefix NPM Event", isCompleted: true, durationInMinutes: 30), + Event(ObjectId(), name: "$prefix NPM Meeting", isCompleted: false, durationInMinutes: 10), + Event(ObjectId(), name: "$prefix Some other event", isCompleted: true, durationInMinutes: 15), + ]); + }); + expect(query.length, 3); + await realm.syncSession.waitForUpload(); + + // Remove all the data from realm file after synchronization completes + realm.subscriptions.update((mutableSubscriptions) => mutableSubscriptions.clear()); + await realm.subscriptions.waitForSynchronization(); + expect(query.length, 0); + + // Subscribing will download only the objects with names containing 'NPM' + final subscribedByName = await realm.query('$byTestRun AND name CONTAINS \$0', ["NPM"]).subscribe(); + expect(subscribedByName.length, 2); + + // Adding subscription by duration on top of downloaded objects by name + // will remove the objects, which don't match duration < 20, from the local realm + final subscribedByNameAndDuration = await subscribedByName.query(r'durationInMinutes < $0', [20]).subscribe(); + expect(subscribedByNameAndDuration.length, 1); + expect(subscribedByNameAndDuration[0].durationInMinutes, 10); + expect(subscribedByNameAndDuration[0].name, contains("NPM")); + + // Query local realm by duration + final filteredByDuration = realm.query("$byTestRun AND durationInMinutes < \$0", [20]); + expect(filteredByDuration.length, 1); // duration 10 only, because there is subscription by name containing 'NPM' and duration < 20 + + // Subscribing only by duration will download all objects with duration < 20 independent on the name + final subscribedByDuration = await filteredByDuration.subscribe(); + expect(subscribedByDuration.length, 2); // duration 10 and 15, because all objects with durations < 20 are downloaded + }); + + test("Using flexible sync subscribe API for local realm throws", () async { + final config = Configuration.local([Event.schema]); + final realm = getRealm(config); + await expectLater( + () => realm.all().subscribe(), throws("subscriptions is only valid on Realms opened with a FlexibleSyncConfiguration")); + expect(() => realm.all().unsubscribe(), throws("unsubscribe is only allowed on Realms opened with a FlexibleSyncConfiguration")); + }); + + baasTest('Flexible sync subscribe API - duplicated subscription', (config) async { + final realm = await getIntegrationRealm(appConfig: config); + final subscriptionName1 = "sub1"; + final subscriptionName2 = "sub2"; + final query1 = realm.all(); + final query2 = realm.query("name = \$0", ["some name"]); + + await query1.subscribe(name: subscriptionName1); + expect(realm.subscriptions.length, 1); + + //Replace subscription with query2 using the same name and update flag + await query2.subscribe(name: subscriptionName1, update: true); + expect(realm.subscriptions.length, 1); + expect(realm.subscriptions.findByName(subscriptionName1), isNotNull); + + //Subscribe for the same query2 with different name + await query2.subscribe(name: subscriptionName2); + expect(realm.subscriptions.length, 2); + expect(realm.subscriptions.findByName(subscriptionName1), isNotNull); + expect(realm.subscriptions.findByName(subscriptionName2), isNotNull); + + //Add query subscription with the same name and update=false throws + await expectLater(() => query1.subscribe(name: subscriptionName2), throws("Duplicate subscription with name: $subscriptionName2")); + }); + + baasTest('Flexible sync subscribe/unsubscribe and removeAllUnnamed', (config) async { + final realm = await getIntegrationRealm(appConfig: config); + final subscriptionName1 = "sub1"; + final subscriptionName2 = "sub2"; + final query = realm.all(); + final queryFiltered = realm.query("name='x'"); + + final unnamedResults = await query.subscribe(); // +1 unnamed subscription + await query.subscribe(); // +0 subscription already exists + await realm.all().subscribe(); // +0 subscription already exists + await queryFiltered.subscribe(); // +1 unnamed subscription + final namedResults1 = await query.subscribe(name: subscriptionName1); // +1 named subscription + final namedResults2 = await query.subscribe(name: subscriptionName2); // +1 named subscription + expect(realm.subscriptions.length, 4); + + expect(query.unsubscribe(), isFalse); // -0 (query is not a subscription) + expect(realm.subscriptions.length, 4); + + expect(unnamedResults.unsubscribe(), isTrue); // -1 unnamed subscription on query + expect(realm.subscriptions.length, 3); + expect(realm.subscriptions.find(queryFiltered), isNotNull); + expect(realm.subscriptions.findByName(subscriptionName1), isNotNull); + expect(realm.subscriptions.findByName(subscriptionName2), isNotNull); + + realm.subscriptions.update((mutableSubscriptions) => mutableSubscriptions.clear(unnamedOnly: true)); // -1 unnamed subscription on queryFiltered + + expect(realm.subscriptions.length, 2); + expect(realm.subscriptions.findByName(subscriptionName1), isNotNull); + expect(realm.subscriptions.findByName(subscriptionName2), isNotNull); + + expect(namedResults1.unsubscribe(), isTrue); // -1 named subscription sub1 + expect(realm.subscriptions.length, 1); + expect(realm.subscriptions.findByName(subscriptionName2), isNotNull); + + expect(namedResults2.unsubscribe(), isTrue); // -1 named subscription + expect(realm.subscriptions.length, 0); + }); + + baasTest('Flexible sync subscribe/unsubscribe API wait for download', (configuration) async { + int count = 2; + RealmResults query = await _getQueryToSubscribeForDownload(configuration, count); + final results = await query.subscribe(waitForSyncMode: WaitForSyncMode.never); + expect(results.length, 0); // didn't wait for downloading because of WaitForSyncMode.never + + final second = await query.subscribe(waitForSyncMode: WaitForSyncMode.always); + expect(second.length, count); // product_1 and product_21 + }); + + baasTest('Flexible sync subscribe/unsubscribe cancellation token', (configuration) async { + RealmResults query = await _getQueryToSubscribeForDownload(configuration, 3); + + // Wait Always if timeout expired + final timeoutCancellationToken = TimeoutCancellationToken(Duration(microseconds: 0)); + await expectLater( + () async => await query.subscribe(waitForSyncMode: WaitForSyncMode.always, cancellationToken: timeoutCancellationToken), + throwsA(isA()), + ); + + // Wait Always but cancel berfore + final cancellationToken = CancellationToken(); + cancellationToken.cancel(); + await expectLater( + query.subscribe(waitForSyncMode: WaitForSyncMode.always, cancellationToken: cancellationToken), + throwsA(isA()), + ); + + // Wait Never but cancel before + final cancellationToken1 = CancellationToken(); + cancellationToken1.cancel(); + await expectLater( + query.subscribe(waitForSyncMode: WaitForSyncMode.never, cancellationToken: cancellationToken1), + throwsA(isA()), + ); + + // Wait Always but cancel after + final cancellationToken2 = CancellationToken(); + final subFuture = query.subscribe(waitForSyncMode: WaitForSyncMode.always, cancellationToken: cancellationToken2); + cancellationToken2.cancel(); + + expect( + () async => await subFuture, + throwsA(isA()), + ); + }); +} + +Future> _getQueryToSubscribeForDownload(AppConfiguration configuration, int takeCount) async { + final prefix = generateRandomString(4); + final byTestRun = "name BEGINSWITH '$prefix'"; + App app = App(configuration); + final userA = await app.logIn(Credentials.anonymous(reuseCredentials: false)); + final configA = Configuration.flexibleSync(userA, getSyncSchema()); + final realmA = getRealm(configA); + await realmA.query(byTestRun).subscribe(); + List names = []; + realmA.write(() { + for (var i = 0; i < 20; i++) { + final name = "${prefix}_${i + 1}"; + names.add(name); + realmA.add(Product(ObjectId(), name)); + } + }); + await realmA.syncSession.waitForUpload(); + realmA.close(); + + final userB = await app.logIn(Credentials.anonymous(reuseCredentials: false)); + final configB = Configuration.flexibleSync(userB, getSyncSchema()); + final realmB = getRealm(configB); + final query = realmB.query('$byTestRun AND name IN \$0', [names.take(takeCount)]); + + return query; } diff --git a/test/test.dart b/test/test.dart index 2df22d03a5..e58b1c4e61 100644 --- a/test/test.dart +++ b/test/test.dart @@ -396,9 +396,12 @@ void test(String name, dynamic Function() testFunction, {dynamic skip, Map setupTests(List? args) async { Realm.logger = Logger.detached('test run') ..level = Level.ALL ..onRecord.listen((record) { + if (record.level.value >= RealmLogLevel.warn.value) { + print('${record.time} ${record.level.name}: ${record.message}'); + } + testing.printOnFailure('${record.time} ${record.level.name}: ${record.message}'); }); @@ -645,13 +652,9 @@ Future baasTest( skip = shouldSkip(baasUri, skip); test(name, () async { - try { - final config = await getAppConfig(appName: appName); - await testFunction(config); - } catch (error) { - printSplunkLogLink(appName, baasUri); - rethrow; - } + printSplunkLogLink(appName, baasUri); + final config = await getAppConfig(appName: appName); + await testFunction(config); }, skip: skip); } @@ -708,7 +711,7 @@ Future getIntegrationRealm({App? app, ObjectId? differentiator, AppConfig app ??= App(appConfig ?? await getAppConfig()); final user = await getIntegrationUser(app); - final config = Configuration.flexibleSync(user, getSyncSchema()); + final config = Configuration.flexibleSync(user, getSyncSchema())..sessionStopPolicy = SessionStopPolicy.immediately; final realm = getRealm(config); if (differentiator != null) { realm.subscriptions.update((mutableSubscriptions) { @@ -823,15 +826,16 @@ void printSplunkLogLink(AppNames appName, String? uriVariable) { if (uriVariable == null) { return; } + final app = baasApps[appName.name] ?? baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); final baasUri = Uri.parse(uriVariable); - print("App service name: ${app.uniqueName}"); + testing.printOnFailure("App service name: ${app.uniqueName}"); final host = baasUri.host.endsWith('-qa.mongodb.com') ? "-qa" : ""; final splunk = Uri.encodeFull( "https://splunk.corp.mongodb.com/en-US/app/search/search?q=search index=baas$host \"${app.uniqueName}-*\" | reverse | top error msg&earliest=-7d&latest=now&display.general.type=visualizations"); - print("Splunk logs: $splunk"); + testing.printOnFailure("Splunk logs: $splunk"); } /// Schema list for default app service From b5f5415bce9045a83b1fd2835c8bdf9bf0524050 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 15 Nov 2023 03:11:38 +0100 Subject: [PATCH 11/16] Upgrade to Core 13.23.4 (#1427) --- CHANGELOG.md | 4 ++++ lib/src/native/realm_bindings.dart | 14 ++++++++++---- lib/src/native/realm_core.dart | 5 +++-- lib/src/scheduler.dart | 4 +++- src/realm-core | 2 +- src/realm_dart_scheduler.cpp | 11 +++++------ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac28006b8..aff9d0a543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ * Receiving a `write_not_allowed` error from the server would have led to a crash. (Core 13.22.0) * Fix interprocess locking for concurrent realm file access resulting in a interprocess deadlock on FAT32/exFAT filesystems. (Core 13.23.0) * Fixed RealmObject not overriding `hashCode`, which would lead to sets of RealmObjects potentially containing duplicates. ([#1418](https://github.com/realm/realm-dart/issues/1418)) +* `realm.subscriptions.waitForSynchronization` will now correctly receive an error if a fatal session error occurs that would prevent it from ever completing. Previously the future would never resolve. (Core 13.23.3) +* Fixed FLX subscriptions not being sent to the server if the session was interrupted during bootstrapping. (Core 13.23.3) +* Fixed application crash with 'KeyNotFound' exception when subscriptions are marked complete after a client reset. (Core 13.23.3) +* A crash at a very specific time during a DiscardLocal client reset on a FLX Realm could leave subscriptions in an invalid state. (Core 13.23.4) ### Compatibility * Realm Studio: 13.0.0 or later. diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index 8a5ef8b252..7de5bcec78 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -8044,7 +8044,7 @@ class RealmLibrary { /// be called each time the notify callback passed to the scheduler /// is invoked. void realm_scheduler_perform_work( - ffi.Pointer arg0, + ffi.Pointer arg0, ) { return _realm_scheduler_perform_work( arg0, @@ -8053,10 +8053,10 @@ class RealmLibrary { late final _realm_scheduler_perform_workPtr = _lookup< ffi - .NativeFunction)>>( + .NativeFunction)>>( 'realm_scheduler_perform_work'); late final _realm_scheduler_perform_work = _realm_scheduler_perform_workPtr - .asFunction)>(); + .asFunction)>(); /// For platforms with no default scheduler implementation, register a factory /// function which can produce custom schedulers. If there is a platform-specific @@ -11943,7 +11943,9 @@ typedef realm_scheduler_is_same_as_func_t = ffi.Pointer< ffi.Bool Function(ffi.Pointer scheduler_userdata_1, ffi.Pointer scheduler_userdata_2)>>; typedef realm_scheduler_notify_func_t = ffi.Pointer< - ffi.NativeFunction userdata)>>; + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer userdata, + ffi.Pointer work_queue)>>; typedef realm_scheduler_t = realm_scheduler; final class realm_schema extends ffi.Opaque {} @@ -12373,4 +12375,8 @@ final class realm_websocket_observer extends ffi.Opaque {} typedef realm_websocket_observer_t = realm_websocket_observer; +final class realm_work_queue extends ffi.Opaque {} + +typedef realm_work_queue_t = realm_work_queue; + final class shared_realm extends ffi.Opaque {} diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 5f02efb8c0..4d6a34675c 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -651,8 +651,9 @@ class _RealmCore { return SchedulerHandle._(schedulerPtr); } - void invokeScheduler(SchedulerHandle schedulerHandle) { - _realmLib.realm_scheduler_perform_work(schedulerHandle._pointer); + void invokeScheduler(int workQueue) { + final queuePointer = Pointer.fromAddress(workQueue); + _realmLib.realm_scheduler_perform_work(queuePointer); } RealmHandle openRealm(Configuration config) { diff --git a/lib/src/scheduler.dart b/lib/src/scheduler.dart index f5e98ee1e2..6918598c95 100644 --- a/lib/src/scheduler.dart +++ b/lib/src/scheduler.dart @@ -38,8 +38,10 @@ class Scheduler { final level = message[0] as int; final text = message[1] as String; Realm.logger.log(LevelExt.fromInt(level), text); + } else if (message is int) { + realmCore.invokeScheduler(message); } else { - realmCore.invokeScheduler(handle); + Realm.logger.log(RealmLogLevel.error, 'Unexpected Scheduler message type: ${message.runtimeType} - $message'); } }; diff --git a/src/realm-core b/src/realm-core index 56a048d9f4..a2e743a388 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit 56a048d9f450445f2d52c7405f3019a533bdaa3f +Subproject commit a2e743a388a3d70230ade165e0edc1d2f025bc95 diff --git a/src/realm_dart_scheduler.cpp b/src/realm_dart_scheduler.cpp index 537105decf..5c75e2e9a7 100644 --- a/src/realm_dart_scheduler.cpp +++ b/src/realm_dart_scheduler.cpp @@ -33,7 +33,8 @@ struct SchedulerData { void* callback_userData = nullptr; realm_free_userdata_func_t free_userData_func = nullptr; - SchedulerData(uint64_t isolate, Dart_Port dartPort) : port(dartPort), threadId(std::this_thread::get_id()), isolateId(isolate) + SchedulerData(uint64_t isolate, Dart_Port dartPort) + : port(dartPort), threadId(std::this_thread::get_id()), isolateId(isolate) {} }; @@ -46,9 +47,9 @@ void realm_dart_scheduler_free_userData(void* userData) { } //This can be invoked on any thread. -void realm_dart_scheduler_notify(void* userData) { +void realm_dart_scheduler_notify(void* userData, realm_work_queue_t* work_queue) { auto& schedulerData = *static_cast(userData); - std::uintptr_t pointer = reinterpret_cast(userData); + std::uintptr_t pointer = reinterpret_cast(work_queue); Dart_PostInteger_DL(schedulerData.port, pointer); } @@ -84,14 +85,12 @@ bool realm_dart_scheduler_can_deliver_notifications(void* userData) { RLM_API realm_scheduler_t* realm_dart_create_scheduler(uint64_t isolateId, Dart_Port port) { SchedulerData* schedulerData = new SchedulerData(isolateId, port); - realm_scheduler_t* realm_scheduler = realm_scheduler_new(schedulerData, + return realm_scheduler_new(schedulerData, realm_dart_scheduler_free_userData, realm_dart_scheduler_notify, realm_dart_scheduler_is_on_thread, realm_dart_scheduler_is_same_as, realm_dart_scheduler_can_deliver_notifications); - - return realm_scheduler; } //Used for debugging From d588e140bbd566e4924c1da2c4132d2104370b63 Mon Sep 17 00:00:00 2001 From: Realm CI Date: Wed, 15 Nov 2023 15:48:56 +0100 Subject: [PATCH 12/16] [Release 1.6.0] (#1428) * [Release 1.6.0] * Restore topic.md --------- Co-authored-by: nirinchev Co-authored-by: Nikola Irinchev --- CHANGELOG.md | 2 +- common/pubspec.yaml | 2 +- flutter/realm_flutter/example/pubspec.yaml | 2 +- flutter/realm_flutter/ios/realm.podspec | 2 +- flutter/realm_flutter/macos/realm.podspec | 2 +- flutter/realm_flutter/pubspec.yaml | 2 +- flutter/realm_flutter/tests/pubspec.yaml | 2 +- generator/pubspec.yaml | 2 +- lib/src/cli/metrics/metrics_command.dart | 2 +- lib/src/native/realm_core.dart | 2 +- pubspec.yaml | 2 +- src/realm_dart.cpp | 2 +- topic.md | 0 13 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 topic.md diff --git a/CHANGELOG.md b/CHANGELOG.md index aff9d0a543..1835853c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## vNext (TBD) +## 1.6.0 (2023-11-15) ### Enhancements * Support for performing geo spatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox` and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations ([#1389](https://github.com/realm/realm-dart/pull/1389)) diff --git a/common/pubspec.yaml b/common/pubspec.yaml index 9d4ce753f9..9b802c2901 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -3,7 +3,7 @@ description: >- Hosts the common code shared between realm, realm_dart and realm_generator packages. This package is part of the official Realm Flutter and Realm Dart SDKs. -version: 1.5.0 +version: 1.6.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart diff --git a/flutter/realm_flutter/example/pubspec.yaml b/flutter/realm_flutter/example/pubspec.yaml index 6c3165bf0d..b8e3457a11 100644 --- a/flutter/realm_flutter/example/pubspec.yaml +++ b/flutter/realm_flutter/example/pubspec.yaml @@ -1,6 +1,6 @@ name: realm_example description: Demonstrates how to use the Realm SDK for Flutter. -version: 1.5.0 +version: 1.6.0 # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. diff --git a/flutter/realm_flutter/ios/realm.podspec b/flutter/realm_flutter/ios/realm.podspec index 865ab328ae..fef38f9666 100644 --- a/flutter/realm_flutter/ios/realm.podspec +++ b/flutter/realm_flutter/ios/realm.podspec @@ -19,7 +19,7 @@ puts "bundleId is #{bundleId}" Pod::Spec.new do |s| s.name = 'realm' - s.version = '1.5.0' + s.version = '1.6.0' s.summary = 'The official Realm SDK for Flutter' s.description = <<-DESC Realm is a mobile database - an alternative to SQLite and key-value stores. diff --git a/flutter/realm_flutter/macos/realm.podspec b/flutter/realm_flutter/macos/realm.podspec index d18ad0eaa6..360fc00279 100644 --- a/flutter/realm_flutter/macos/realm.podspec +++ b/flutter/realm_flutter/macos/realm.podspec @@ -36,7 +36,7 @@ puts "bundleId is #{bundleId}" Pod::Spec.new do |s| s.name = 'realm' - s.version = '1.5.0' + s.version = '1.6.0' s.summary = 'The official Realm SDK for Flutter' s.description = <<-DESC Realm is a mobile database - an alternative to SQLite and key-value stores. diff --git a/flutter/realm_flutter/pubspec.yaml b/flutter/realm_flutter/pubspec.yaml index f81972d175..f036bd0107 100644 --- a/flutter/realm_flutter/pubspec.yaml +++ b/flutter/realm_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: realm description: The official Realm SDK for Flutter. Realm is a mobile database - an alternative to SQLite and key-value stores. -version: 1.5.0 +version: 1.6.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart diff --git a/flutter/realm_flutter/tests/pubspec.yaml b/flutter/realm_flutter/tests/pubspec.yaml index 4ee4feb077..917a51f456 100644 --- a/flutter/realm_flutter/tests/pubspec.yaml +++ b/flutter/realm_flutter/tests/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. publish_to: "none" -version: 1.5.0 +version: 1.6.0 environment: sdk: ^3.0.2 diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index 00225b9f00..9e30d548c0 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -3,7 +3,7 @@ description: >- Generates RealmObject classes from Realm data model classes. This package is part of the official Realm Flutter and Realm Dart SDKs. -version: 1.5.0 +version: 1.6.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart diff --git a/lib/src/cli/metrics/metrics_command.dart b/lib/src/cli/metrics/metrics_command.dart index 8896496349..6e7d5365a5 100644 --- a/lib/src/cli/metrics/metrics_command.dart +++ b/lib/src/cli/metrics/metrics_command.dart @@ -32,7 +32,7 @@ import 'options.dart'; import '../common/utils.dart'; // stamped into the library by the build system (see prepare-release.yml) -const realmCoreVersion = '13.17.2'; +const realmCoreVersion = '13.23.4'; class MetricsCommand extends Command { @override diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 4d6a34675c..6b1fc71858 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -88,7 +88,7 @@ final _pluginLib = () { }(); // stamped into the library by the build system (see prepare-release.yml) -const libraryVersion = '1.5.0'; +const libraryVersion = '1.6.0'; _RealmCore realmCore = _RealmCore(); diff --git a/pubspec.yaml b/pubspec.yaml index 4be7400154..f6c29a4f00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: realm_dart description: The official Realm SDK for Dart. Realm is a mobile database - an alternative to SQLite and key-value stores. -version: 1.5.0 +version: 1.6.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart diff --git a/src/realm_dart.cpp b/src/realm_dart.cpp index f299550025..80d3014f1f 100644 --- a/src/realm_dart.cpp +++ b/src/realm_dart.cpp @@ -118,7 +118,7 @@ RLM_API void realm_dart_invoke_unlock_callback(bool success, void* unlockFunc) { // Stamped into the library by the build system (see prepare-release.yml) // Keep this method as it is written and do not format it. // We have a github workflow that looks for and replaces this string as it is written here. -RLM_API const char* realm_dart_library_version() { return "1.5.0"; } +RLM_API const char* realm_dart_library_version() { return "1.6.0"; } //for debugging only // RLM_API void realm_dart_gc() { diff --git a/topic.md b/topic.md new file mode 100644 index 0000000000..e69de29bb2 From e699ed04973184fb868c79ec710a1ffc200ca1d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:49:09 +0000 Subject: [PATCH 13/16] Add vNext Changelog header (#1429) Co-authored-by: nirinchev --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1835853c04..1903a5dbd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## vNext (TBD) + +### Enhancements +* None + +### Fixed +* None + +### Compatibility +* Realm Studio: 13.0.0 or later. + +### Internal +* Using Core x.y.z. + ## 1.6.0 (2023-11-15) ### Enhancements From 82cff56dc00cfd419bd7ca2151086f8e6f217da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 28 Nov 2023 17:14:30 +0100 Subject: [PATCH 14/16] kn/update analyzer (#1365) * Update flutter and analyzer constraints to support analyzer ^6.0.0 * kevmoo releaseb build_cli 2.2.2 with support for analyzer ^6.0.0. Git dep no longer needed * Update CHANGELOG * Remove all use of deprecated properties * Revert to stable sdk on linux as well * fix test * Update ffi bindings --------- Co-authored-by: Nikola Irinchev --- .github/workflows/dart-desktop-tests.yml | 2 +- CHANGELOG.md | 2 ++ common/pubspec.yaml | 2 +- example/pubspec.yaml | 2 +- ffigen/pubspec.yaml | 4 ++-- flutter/realm_flutter/example/pubspec.yaml | 4 ++-- flutter/realm_flutter/pubspec.yaml | 6 +++--- .../tests/macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- flutter/realm_flutter/tests/pubspec.yaml | 11 ++++++++--- generator/lib/src/pseudo_type.dart | 2 +- generator/pubspec.yaml | 4 ++-- lib/src/native/realm_bindings.dart | 11 ++++------- pubspec.yaml | 4 ++-- test/realm_logger_test.dart | 2 +- 15 files changed, 32 insertions(+), 28 deletions(-) diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index 72a7fe3a29..c8b9df310b 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -58,7 +58,7 @@ jobs: - name : Setup Dart SDK uses: dart-lang/setup-dart@main with: - sdk: 'stable' + sdk: stable architecture: ${{ inputs.architecture == 'arm' && 'arm64' || 'x64'}} - name: Install dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 1903a5dbd2..b9be9cefb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,8 @@ ### Compatibility * Realm Studio: 13.0.0 or later. +* Flutter: ^3.13.0 +* Dart: ^3.1.0 ### Internal * Using Core 13.17.2. diff --git a/common/pubspec.yaml b/common/pubspec.yaml index 9b802c2901..7f96435115 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -12,7 +12,7 @@ issue_tracker: https://github.com/realm/realm-dart/issues publish_to: none environment: - sdk: ^3.0.2 + sdk: ^3.1.0 dependencies: objectid: ^3.0.0 diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2fd8cfeca3..ea3493ac95 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A simple command-line application using Realm Dart SDK. publish_to: none environment: - sdk: ^3.0.2 + sdk: ^3.1.0 dependencies: realm_dart: diff --git a/ffigen/pubspec.yaml b/ffigen/pubspec.yaml index a46ef209eb..ff75bcd9a9 100644 --- a/ffigen/pubspec.yaml +++ b/ffigen/pubspec.yaml @@ -5,7 +5,7 @@ description: >- publish_to: none environment: - sdk: ^3.0.2 + sdk: ^3.1.0 dev_dependencies: - ffigen: ^8.0.2 \ No newline at end of file + ffigen: ^9.0.1 \ No newline at end of file diff --git a/flutter/realm_flutter/example/pubspec.yaml b/flutter/realm_flutter/example/pubspec.yaml index b8e3457a11..18de3c7a1a 100644 --- a/flutter/realm_flutter/example/pubspec.yaml +++ b/flutter/realm_flutter/example/pubspec.yaml @@ -7,8 +7,8 @@ version: 1.6.0 publish_to: "none" environment: - sdk: ^3.0.2 - flutter: ^3.10.2 + sdk: ^3.1.0 + flutter: ^3.13.0 dependencies: flutter: diff --git a/flutter/realm_flutter/pubspec.yaml b/flutter/realm_flutter/pubspec.yaml index f036bd0107..b72e7ad105 100644 --- a/flutter/realm_flutter/pubspec.yaml +++ b/flutter/realm_flutter/pubspec.yaml @@ -9,8 +9,8 @@ issue_tracker: https://github.com/realm/realm-dart/issues publish_to: none environment: - sdk: ^3.0.2 - flutter: ^3.10.2 + sdk: ^3.1.0 + flutter: ^3.13.0 dependencies: flutter: @@ -39,7 +39,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_cli: ^2.1.3 + build_cli: ^2.2.2 json_serializable: ^6.1.0 lints: ^2.0.0 diff --git a/flutter/realm_flutter/tests/macos/Runner.xcodeproj/project.pbxproj b/flutter/realm_flutter/tests/macos/Runner.xcodeproj/project.pbxproj index 51b31764c4..0d2107c334 100644 --- a/flutter/realm_flutter/tests/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/realm_flutter/tests/macos/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/flutter/realm_flutter/tests/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/realm_flutter/tests/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 33e15ac158..685851c1ed 100644 --- a/flutter/realm_flutter/tests/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/realm_flutter/tests/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ null; + Element? get element2 => _never; @override Element? get element => null; diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index 9e30d548c0..4fd8928774 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -12,10 +12,10 @@ issue_tracker: https://github.com/realm/realm-dart/issues publish_to: none environment: - sdk: ^3.0.2 + sdk: ^3.1.0 dependencies: - analyzer: ^5.13.0 + analyzer: ^6.0.0 build_resolvers: ^2.0.9 build: ^2.0.0 dart_style: ^2.2.0 diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index 7de5bcec78..23f0a9dec0 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -4686,9 +4686,8 @@ class RealmLibrary { } late final _realm_freezePtr = _lookup< - ffi - .NativeFunction Function(ffi.Pointer)>>( - 'realm_freeze'); + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>('realm_freeze'); late final _realm_freeze = _realm_freezePtr .asFunction Function(ffi.Pointer)>(); @@ -8945,8 +8944,7 @@ class RealmLibrary { late final _realm_sync_client_config_set_user_agent_application_infoPtr = _lookup< - ffi - .NativeFunction< + ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer)>>( 'realm_sync_client_config_set_user_agent_application_info'); @@ -9715,8 +9713,7 @@ class RealmLibrary { late final _realm_sync_session_register_connection_state_change_callbackPtr = _lookup< - ffi - .NativeFunction< + ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, realm_sync_connection_state_changed_func_t, diff --git a/pubspec.yaml b/pubspec.yaml index f6c29a4f00..9c7d4e5b64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ issue_tracker: https://github.com/realm/realm-dart/issues publish_to: none environment: - sdk: ^3.0.2 + sdk: ^3.1.0 dependencies: args: ^2.3.0 @@ -34,7 +34,7 @@ dependencies: cancellation_token: ^2.0.0 dev_dependencies: - build_cli: ^2.2.0 + build_cli: ^2.2.2 json_serializable: ^6.3.1 lints: ^2.0.0 test: ^1.14.3 diff --git a/test/realm_logger_test.dart b/test/realm_logger_test.dart index 7d527aceae..fef9989fbf 100644 --- a/test/realm_logger_test.dart +++ b/test/realm_logger_test.dart @@ -220,6 +220,6 @@ Future main([List? args]) async { ]; //first isolate should have collected all the messages - expect(actual, expected); + expect(actual, containsAllInOrder(expected)); }); } From e27a42fd25c1b21e66a232d8d15a87a21a1782fd Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 29 Nov 2023 04:27:33 +0100 Subject: [PATCH 15/16] Wait for initial sync to complete before starting the tests (#1435) * Wait for initial sync to complete before starting the tests * Add print statements * Wait for initial sync after an app is setup * Rework indexed test --- lib/src/cli/atlas_apps/baas_client.dart | 31 ++++- lib/src/results.dart | 7 +- test/indexed_test.dart | 171 +++++++++++------------- test/subscription_test.dart | 1 + test/test.dart | 21 ++- 5 files changed, 128 insertions(+), 103 deletions(-) diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 298280cd8a..f0d78f9720 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -159,6 +159,15 @@ class BaasClient { return result; } + Future waitForInitialSync(BaasApp app) async { + while (!await _isSyncComplete(app)) { + print('Initial sync for ${app.name} is incomplete. Waiting 5 seconds.'); + await Future.delayed(Duration(seconds: 5)); + } + + print('Initial sync for ${app.name} is complete.'); + } + Future _createAppIfNotExists(Map existingApps, String appName, String appSuffix, {String? confirmationType}) async { final existingApp = existingApps[appName]; if (existingApp == null) { @@ -166,6 +175,26 @@ class BaasClient { } } + Future _isSyncComplete(BaasApp app) async { + try { + final response = await _get('groups/$_groupId/apps/$app/sync/progress'); + + Map progressInfo = response['progress']; + for (final key in progressInfo.keys) { + final namespaceComplete = progressInfo[key]['complete'] as bool; + + if (!namespaceComplete) { + return false; + } + } + + return true; + } catch (e) { + print(e); + return false; + } + } + Future> _getApps() async { final apps = await _get('groups/$_groupId/apps') as List; return apps @@ -603,7 +632,7 @@ class BaasApp { Object? error; BaasApp(this.appId, this.clientAppId, this.name, this.uniqueName); - + BaasApp._empty(this.name) : appId = "", clientAppId = "", diff --git a/lib/src/results.dart b/lib/src/results.dart index 84e53de46d..0c28f6eed1 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -203,9 +203,10 @@ extension RealmResultsOfRealmObject on RealmResults { CancellationToken? cancellationToken, bool update = false, }) async { - Subscription? existingSubscription = name == null ? realm.subscriptions.find(this) : realm.subscriptions.findByName(name); + final subscriptions = realm.subscriptions; + Subscription? existingSubscription = name == null ? subscriptions.find(this) : subscriptions.findByName(name); late Subscription updatedSubscription; - realm.subscriptions.update((mutableSubscriptions) { + subscriptions.update((mutableSubscriptions) { updatedSubscription = mutableSubscriptions.add(this, name: name, update: update); }); bool shouldWait = waitForSyncMode == WaitForSyncMode.always || @@ -216,7 +217,7 @@ extension RealmResultsOfRealmObject on RealmResults { throw cancellationToken.exception!; } if (shouldWait) { - await realm.subscriptions.waitForSynchronization(cancellationToken); + await subscriptions.waitForSynchronization(cancellationToken); await realm.syncSession.waitForDownload(cancellationToken); } return _SubscribedRealmResult._(this, subscriptionName: name); diff --git a/test/indexed_test.dart b/test/indexed_test.dart index c3b7e44d82..d92ff818f2 100644 --- a/test/indexed_test.dart +++ b/test/indexed_test.dart @@ -81,8 +81,6 @@ class FtsTestData { FtsTestData(this.query, this.expectedResults); } -typedef QueryBuilder = RealmResults Function(U value); - const String animalFarm = 'Animal Farm'; const String lordOfTheRings = 'The Lord of the Rings'; const String lordOfTheFlies = 'Lord of the Flies'; @@ -92,115 +90,96 @@ const String silmarillion = 'The Silmarillion'; void main([List? args]) { setupTests(args); - test('Indexed faster', () { - final config = Configuration.local([WithIndexes.schema, NoIndexes.schema]); - Realm.deleteRealm(config.path); - final realm = Realm(config); - - const max = 100000; - final allIndexed = realm.all(); - final allNotIndexed = realm.all(); - - expect(realm.all().length, 0); - - intFactory(int i) => i.hashCode; - boolFactory(int i) => i % 2 == 0; - stringFactory(int i) => '${i.hashCode} $i'; - timestampFactory(int i) => DateTime.fromMillisecondsSinceEpoch(i.hashCode); - objectIdFactory(int i) => ObjectId.fromValues(i.hashCode * 1000000, i.hashCode, i); - uuidFactory(int i) => Uuid.fromBytes(Uint8List(16).buffer..asByteData().setInt64(0, i.hashCode)); - - final indexed = List.generate( - max, - (i) => WithIndexes( - intFactory(i), - boolFactory(i), - stringFactory(i), - timestampFactory(i), - objectIdFactory(i), - uuidFactory(i), - ), - ); - realm.write(() => realm.addAll(indexed)); - - expect(allIndexed.length, max); - - final notIndexed = List.generate( - max, - (i) => NoIndexes( - intFactory(i), - boolFactory(i), - stringFactory(i), - timestampFactory(i), - objectIdFactory(i), - uuidFactory(i), - ), - ); - realm.write(() => realm.addAll(notIndexed)); - - expect(allNotIndexed.length, max); - - // Inefficient, but fast enough for this test - final searchOrder = (List.generate(max, (i) => i)..shuffle(Random(42))).take(1000).toList(); - - QueryBuilder builder(RealmResults results, String fieldName) { - return (U value) => results.query('$fieldName == \$0', [value]); - } + intFactory(int i) => i.hashCode; + boolFactory(int i) => i % 2 == 0; + stringFactory(int i) => '${i.hashCode} $i'; + timestampFactory(int i) => DateTime.fromMillisecondsSinceEpoch(i.hashCode); + objectIdFactory(int i) => ObjectId.fromValues(i.hashCode * 1000000, i.hashCode, i); + uuidFactory(int i) => Uuid.fromBytes(Uint8List(16).buffer..asByteData().setInt64(0, i.hashCode)); + + // skip timestamp for now, as timestamps are not indexed properly it seems + final indexedTestData = [('anInt', intFactory), ('string', stringFactory), ('objectId', objectIdFactory), ('uuid', uuidFactory)]; + + for (final testCase in indexedTestData) { + test('Indexed faster: ${testCase.$1}', () { + final config = Configuration.local([WithIndexes.schema, NoIndexes.schema]); + Realm.deleteRealm(config.path); + final realm = Realm(config); + const max = 100000; + final allIndexed = realm.all(); + final allNotIndexed = realm.all(); + expect(allIndexed.length, 0); + expect(allNotIndexed.length, 0); + + final indexed = List.generate( + max, + (i) => WithIndexes( + intFactory(i), + boolFactory(i), + stringFactory(i), + timestampFactory(i), + objectIdFactory(i), + uuidFactory(i), + ), + ); + realm.write(() => realm.addAll(indexed)); + + expect(allIndexed.length, max); + + final notIndexed = List.generate( + max, + (i) => NoIndexes( + intFactory(i), + boolFactory(i), + stringFactory(i), + timestampFactory(i), + objectIdFactory(i), + uuidFactory(i), + ), + ); + realm.write(() => realm.addAll(notIndexed)); + + expect(allNotIndexed.length, max); + + // Inefficient, but fast enough for this test + final searchOrder = (List.generate(max, (i) => i)..shuffle(Random(42))).map((i) => testCase.$2(i)).take(1000).toList(); + + @pragma('vm:no-interrupts') + Duration measureSpeed(RealmResults results) { + final queries = searchOrder.map((v) => results.query('${testCase.$1} == \$0', [v])).toList(); // pre-calculate queries + final found = []; + + final sw = Stopwatch()..start(); + for (final q in queries) { + found.add(q.singleOrNull); // evaluate query + } + final timing = sw.elapsed; - @pragma('vm:no-interrupts') - Duration measureSpeed( - RealmResults results, - String fieldName, - U Function(int index) indexToValue, - ) { - final queryBuilder = builder(results, fieldName); - final queries = searchOrder.map((i) => queryBuilder(indexToValue(i))).toList(); // pre-calculate queries - final found = []; - - final sw = Stopwatch()..start(); - for (final q in queries) { - found.add(q.singleOrNull); // evaluate query - } - final timing = sw.elapsed; + // check that we found the right objects + for (final f in found) { + expect(f, isNotNull); + } - // check that we found the right objects - for (final f in found) { - expect(f, isNotNull); + return timing; } - return timing; - } - - void compareSpeed( - String fieldName, - U Function(int index) indexToValue, - ) { final lookupCount = searchOrder.length; display(Type type, Duration duration) { - print('$lookupCount lookups of ${'$type'.padRight(12)} on ${fieldName.padRight(10)} : ${duration.inMicroseconds ~/ lookupCount} us/lookup'); + print('$lookupCount lookups of ${'$type'.padRight(12)} on ${testCase.$1.padRight(10)} : ${duration.inMicroseconds ~/ lookupCount} us/lookup'); } - final indexedTime = measureSpeed(allIndexed, fieldName, indexToValue); - final notIndexedTime = measureSpeed(allNotIndexed, fieldName, indexToValue); + final indexedTime = measureSpeed(allIndexed); + final notIndexedTime = measureSpeed(allNotIndexed); try { - // skip timestamp for now, as timestamps are not indexed properly it seems - if (fieldName != 'timestamp') { - expect(indexedTime, lessThan(notIndexedTime)); // indexed should be faster - } + expect(indexedTime, lessThan(notIndexedTime)); // indexed should be faster } catch (_) { display(WithIndexes, indexedTime); // only display if test fails display(NoIndexes, notIndexedTime); rethrow; // rethrow to fail test } - } - - compareSpeed('anInt', intFactory); - compareSpeed('string', stringFactory); - compareSpeed('timestamp', timestampFactory); - compareSpeed('objectId', objectIdFactory); - compareSpeed('uuid', uuidFactory); - }); + }); + } final testCases = [ FtsTestData('lord of the', {lordOfTheFlies, lordOfTheRings, wheelOfTime, silmarillion}), diff --git a/test/subscription_test.dart b/test/subscription_test.dart index 294cda083e..da20ddb7fd 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -654,6 +654,7 @@ Future main([List? args]) async { final query2 = realm.query("name = \$0", ["some name"]); await query1.subscribe(name: subscriptionName1); + expect(realm.subscriptions.version, 1); expect(realm.subscriptions.length, 1); //Replace subscription with query2 using the same name and update flag diff --git a/test/test.dart b/test/test.dart index e58b1c4e61..c5a09b3461 100644 --- a/test/test.dart +++ b/test/test.dart @@ -630,6 +630,8 @@ Future setupBaas() async { final apps = await client.getOrCreateApps(); baasApps.addAll(apps); baasClient = client; + + await _waitForInitialSync(); return true; } catch (error) { print(error); @@ -637,6 +639,17 @@ Future setupBaas() async { } } +Future _waitForInitialSync() async { + try { + final realm = await getIntegrationRealm(); + await realm.syncSession.waitForUpload(); + await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); + } catch (e) { + print(e); + await _waitForInitialSync(); + } +} + @isTest Future baasTest( String name, @@ -670,11 +683,13 @@ dynamic shouldSkip(String? baasUri, dynamic skip) { return skip; } -Future getAppConfig({AppNames appName = AppNames.flexible}) async { +Future getAppConfig({AppNames appName = AppNames.flexible}) => _getAppConfig(appName.name); + +Future _getAppConfig(String appName) async { final baasUrl = arguments[argBaasUrl]; - final app = baasApps[appName.name] ?? - baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); + final app = + baasApps[appName] ?? baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); if (app.error != null) { throw app.error!; } From 2355f74aa45978962ffedbcce749c86188da3e20 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 29 Nov 2023 23:34:07 +0100 Subject: [PATCH 16/16] Don't add the ISRG X1 Root on Android (#1434) * Try to trigger the certificate issue * Only add the certificate on windows * Add a print statement * Wait for initial sync twice * Update changelog --- CHANGELOG.md | 8 ++++---- lib/src/app.dart | 20 +++++++++++--------- test/test.dart | 19 ++++++++++++------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9be9cefb1..ed12a89f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,15 @@ * None ### Fixed -* None +* Fixed an issue where connections to Atlas App Services would fail on Android with a certificate expiration error. (Issue [#1430](https://github.com/realm/realm-dart/issues/1430)) ### Compatibility * Realm Studio: 13.0.0 or later. +* Flutter: ^3.13.0 +* Dart: ^3.1.0 ### Internal -* Using Core x.y.z. +* Using Core 13.23.2. ## 1.6.0 (2023-11-15) @@ -90,8 +92,6 @@ ### Compatibility * Realm Studio: 13.0.0 or later. -* Flutter: ^3.13.0 -* Dart: ^3.1.0 ### Internal * Using Core 13.17.2. diff --git a/lib/src/app.dart b/lib/src/app.dart index 95b359a239..d3dd325fa2 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -64,18 +64,20 @@ he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 -----END CERTIFICATE-----'''; - try { - final context = SecurityContext.defaultContext; - context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM)); - return HttpClient(context: context); - } on TlsException catch (e) { - if (e.osError?.message.contains("CERT_ALREADY_IN_HASH_TABLE") == true) { + if (Platform.isWindows) { + try { + final context = SecurityContext(withTrustedRoots: true); + context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM)); + return HttpClient(context: context); + } on TlsException catch (e) { // certificate is already trusted. Nothing to do here - return HttpClient(); - } else { - rethrow; + if (e.osError?.message.contains("CERT_ALREADY_IN_HASH_TABLE") != true) { + rethrow; + } } } + + return HttpClient(); }(); /// A class exposing configuration options for an [App] diff --git a/test/test.dart b/test/test.dart index c5a09b3461..51d8185ac9 100644 --- a/test/test.dart +++ b/test/test.dart @@ -640,13 +640,18 @@ Future setupBaas() async { } Future _waitForInitialSync() async { - try { - final realm = await getIntegrationRealm(); - await realm.syncSession.waitForUpload(); - await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); - } catch (e) { - print(e); - await _waitForInitialSync(); + while (true) { + try { + print('Validating initial sync is complete...'); + await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); + final realm = await getIntegrationRealm(); + await realm.syncSession.waitForUpload(); + await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); + return; + } catch (e) { + print(e); + await _waitForInitialSync(); + } } }