diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index a7f8fd975..97dc32d71 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -26,10 +26,8 @@ jobs: - macos-10.15 - ubuntu-20.04 dart: - - 2.10.5 - # - 2.9.3 - generator stuck. I remember there was an issue in some dependency but don't remember which one. - - 2.8.4 - - 2.7.2 + - latest + - 2.10.0 # currently the lowest fully supported version (i.e. generator + lib) runs-on: ${{ matrix.os }} steps: # Note: dart-sdk from flutter doesn't work on linux, see https://github.com/flutter/flutter/issues/74599 diff --git a/benchmark/pubspec.yaml b/benchmark/pubspec.yaml index a3e2ce634..25df2e6a0 100644 --- a/benchmark/pubspec.yaml +++ b/benchmark/pubspec.yaml @@ -2,7 +2,7 @@ name: objectbox_benchmark description: Simple ObjectBox-Dart performance benchmark environment: - sdk: ">=2.6.0 <3.0.0" + sdk: ">=2.10.0 <3.0.0" dependencies: objectbox: any diff --git a/flutter_libs/android/build.gradle b/flutter_libs/android/build.gradle index 2b521b47a..661ac01ba 100644 --- a/flutter_libs/android/build.gradle +++ b/flutter_libs/android/build.gradle @@ -12,5 +12,5 @@ android { dependencies { // https://bintray.com/objectbox/objectbox/io.objectbox%3Aobjectbox-android - implementation "io.objectbox:objectbox-android:2.8.0" + implementation "io.objectbox:objectbox-android:2.9.0" } diff --git a/flutter_libs/ios/download-framework.sh b/flutter_libs/ios/download-framework.sh index b2d9341e2..3d668cf27 100755 --- a/flutter_libs/ios/download-framework.sh +++ b/flutter_libs/ios/download-framework.sh @@ -4,19 +4,21 @@ set -euo pipefail # NOTE: run this script before publishing # https://github.com/objectbox/objectbox-swift/releases/ -obxSwiftVersion="1.4.1" +obxSwiftVersion="1.5.0-beta1" dir=$(dirname "$0") -url="https://github.com/objectbox/objectbox-swift/releases/download/v${obxSwiftVersion}/ObjectBox-framework-${obxSwiftVersion}.zip" +url="https://github.com/objectbox/objectbox-swift-spec-staging/releases/download/v1.x/ObjectBox-xcframework-${obxSwiftVersion}.zip" zip="${dir}/fw.zip" curl --location --fail --output "${zip}" "${url}" +frameworkPath=Carthage/Build/ObjectBox.xcframework/ios-arm64/ObjectBox.framework + rm -rf "${dir}/Carthage" unzip "${zip}" -d "${dir}" \ - "Carthage/Build/iOS/ObjectBox.framework/Headers/*" \ - "Carthage/Build/iOS/ObjectBox.framework/ObjectBox" \ - "Carthage/Build/iOS/ObjectBox.framework/Info.plist" + "${frameworkPath}/Headers/*" \ + "${frameworkPath}/ObjectBox" \ + "${frameworkPath}/Info.plist" rm "${zip}" \ No newline at end of file diff --git a/flutter_libs/pubspec.yaml b/flutter_libs/pubspec.yaml index ddbc80edd..4abd13f09 100644 --- a/flutter_libs/pubspec.yaml +++ b/flutter_libs/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://objectbox.io description: ObjectBox is a super-fast NoSQL ACID compliant object database. This package contains flutter runtime libraries for ObjectBox. environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.0 <2.0.0" + sdk: ">=2.9.0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" dependencies: # This is here just to ensure compatibility between objectbox-dart code and the libraries used diff --git a/generator/integration-tests/shared-pubspec.yaml b/generator/integration-tests/shared-pubspec.yaml index a048eb469..67ea8fce1 100644 --- a/generator/integration-tests/shared-pubspec.yaml +++ b/generator/integration-tests/shared-pubspec.yaml @@ -1,7 +1,7 @@ name: objectbox_generator_test environment: - sdk: ">=2.5.0 <3.0.0" + sdk: ">=2.10.0 <3.0.0" dependencies: objectbox: any diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index a6ecad68a..f924ebe91 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -5,7 +5,8 @@ homepage: https://objectbox.io description: ObjectBox binding code generator - finds annotated entities and adds them to the ObjectBox DB model. environment: - sdk: ">=2.5.0 <3.0.0" + # min SDK v2.10.0 (or Flutter v1.22) - there were breaking changes in the analyzer/resolver + sdk: ">=2.10.0 <3.0.0" dependencies: objectbox: 0.11.0 diff --git a/install.sh b/install.sh index a899b60e6..6b1d72d1b 100755 --- a/install.sh +++ b/install.sh @@ -8,7 +8,7 @@ set -eu # * update lib/src/bindings/objectbox.h # * execute pub run ffigen # * have a look at the changed files to see if some call sites need to be updated -cLibVersion=0.11.0 +cLibVersion=0.12.0 os=$(uname) # if there's no tty this is probably part of a docker build - therefore we install the c-api explicitly diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index ca9cccf79..e2d7d0435 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,3 +1,13 @@ +## latest + +* Add `Store.reference` getter and `Store.fromReference()` factory - enabling access to store from multiple isolates. +* Add `Store.subscribe()` and `Store.subscribe()` data change event streams. +* Add multiple `SyncClient` event streams. +* Update to objectbox-c v0.12.0 +* Update to objectbox-android v2.9.0 +* Update to objectbox-swift v1.5.0 +* Increase minimum SDK versions: Flutter v1.20 & Dart v2.9. Code generator already required Flutter v1.22 & Dart v2.10. + ## 0.11.0 (2021-02-01) * Add `ToOne<>` class to wrap related entities. See examples for details. diff --git a/objectbox/Makefile b/objectbox/Makefile index c6cb750fc..1506860ab 100644 --- a/objectbox/Makefile +++ b/objectbox/Makefile @@ -20,7 +20,7 @@ test: ## Test all targets valgrind-test: ## Test all targets with valgrind pub run build_runner build - ../tool/valgrind.sh + tool/valgrind.sh integration-test: ## Execute integration tests cd example/flutter/objectbox_demo/ ; \ diff --git a/objectbox/example/README.md b/objectbox/example/README.md index 1d244b3b7..9170fbf47 100644 --- a/objectbox/example/README.md +++ b/objectbox/example/README.md @@ -96,7 +96,7 @@ omits the argument to `Store(directory: )`, thus using the default - 'objectbox' import 'objectbox.g.dart'; // created by `dart pub run build_runner build` void main() { - var store = Store(getObjectBoxModel()); // Note: getObjectBoxModel() is generated for you in objectbox.g.dart + final store = Store(getObjectBoxModel()); // Note: getObjectBoxModel() is generated for you in objectbox.g.dart // your app code ... diff --git a/objectbox/example/flutter/objectbox_demo/pubspec.yaml b/objectbox/example/flutter/objectbox_demo/pubspec.yaml index c395d4955..0a0e5fbf2 100644 --- a/objectbox/example/flutter/objectbox_demo/pubspec.yaml +++ b/objectbox/example/flutter/objectbox_demo/pubspec.yaml @@ -3,7 +3,8 @@ description: An example project for the objectbox-dart binding. version: 0.3.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.10.0 <3.0.0" + flutter: ">=1.22.0 <2.0.0" dependencies: flutter: diff --git a/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart b/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart index 7b2a36309..b2f68d27e 100644 --- a/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart +++ b/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart @@ -34,9 +34,9 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'OB Example', + title: 'OB Example (sync)', theme: ThemeData(primarySwatch: Colors.blue), - home: MyHomePage(title: 'OB Example'), + home: MyHomePage(title: 'OB Example (sync)'), ); } } @@ -64,11 +64,12 @@ class ViewModel { _query = _box.query().order(dateProp, flags: Order.descending).build(); // TODO configure actual sync server address and authentication + // For configuration and docs, see objectbox/lib/src/sync.dart // 10.0.2.2 is your host PC if an app is run in an Android emulator. // 127.0.0.1 is your host PC if an app is run in an iOS simulator. - // For other options, see objectbox/lib/src/sync.dart + final syncServerIp = Platform.isAndroid ? '10.0.2.2' : '127.0.0.1'; final syncClient = - Sync.client(_store, 'ws://10.0.2.2:9999', SyncCredentials.none()); + Sync.client(_store, 'ws://$syncServerIp:9999', SyncCredentials.none()); syncClient.start(); } @@ -76,11 +77,7 @@ class ViewModel { void removeNote(Note note) => _box.remove(note.id); - // Note: using query.findStream() and sync.client() in the same app is - // currently not supported so this app is currently not working and only - // servers as an example on how and when to start a sync client. - // Stream> get queryStream => _query.findStream(); - Stream> get queryStream => Stream>.empty(); + Stream> get queryStream => _query.findStream(); List get allNotes => _query.find(); @@ -144,6 +141,8 @@ class _MyHomePageState extends State { style: TextStyle( fontSize: 15.0, ), + // Provide a Key for the integration test + key: Key('list_item_${index}'), ), Padding( padding: EdgeInsets.only(top: 5.0), @@ -188,6 +187,8 @@ class _MyHomePageState extends State { InputDecoration(hintText: 'Enter a new note'), controller: _noteInputController, onSubmitted: (value) => _addNote(), + // Provide a Key for the integration test + key: Key('input'), ), ), Padding( @@ -220,6 +221,14 @@ class _MyHomePageState extends State { itemBuilder: _itemBuilder(snapshot.data)); })) ]), + // We need a separate submit button because flutter_driver integration + // test doesn't support submitting a TextField using "enter" key. + // See https://github.com/flutter/flutter/issues/9383 + floatingActionButton: FloatingActionButton( + key: Key('submit'), + onPressed: _addNote, + child: Icon(Icons.add), + ), ); } } diff --git a/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox-model.json b/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox-model.json index 61204203a..ed6697343 100644 --- a/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox-model.json +++ b/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox-model.json @@ -30,7 +30,8 @@ "name": "date", "type": 6 } - ] + ], + "relations": [] } ], "lastEntityId": "1:2802681814019499133", diff --git a/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml b/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml index 90c8c768f..4cf32b9f5 100644 --- a/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml +++ b/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml @@ -3,7 +3,8 @@ description: An example project for the objectbox-dart binding. version: 0.3.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.10.0 <3.0.0" + flutter: ">=1.22.0 <2.0.0" dependencies: flutter: diff --git a/objectbox/lib/objectbox.dart b/objectbox/lib/objectbox.dart index 75aeee343..a75184297 100644 --- a/objectbox/lib/objectbox.dart +++ b/objectbox/lib/objectbox.dart @@ -23,5 +23,13 @@ export 'src/relations/to_many.dart' show ToMany; export 'src/relations/to_one.dart' show ToOne; export 'src/store.dart' show Store; export 'src/sync.dart' - show Sync, SyncClient, SyncCredentials, SyncRequestUpdatesMode, SyncState; + show + Sync, + SyncChange, + SyncClient, + SyncConnectionEvent, + SyncCredentials, + SyncRequestUpdatesMode, + SyncState, + SyncLoginEvent; export 'src/transaction.dart' show TxMode; diff --git a/objectbox/lib/src/bindings/bindings.dart b/objectbox/lib/src/bindings/bindings.dart index 101102d61..80926762f 100644 --- a/objectbox/lib/src/bindings/bindings.dart +++ b/objectbox/lib/src/bindings/bindings.dart @@ -1,6 +1,8 @@ import 'dart:ffi'; import 'dart:io' show Platform; +import '../common.dart'; +import 'helpers.dart'; import 'objectbox-c.dart'; // let files importing bindings.dart also get all the OBX_* types @@ -46,3 +48,34 @@ ObjectBoxC loadObjectBoxLib() { ObjectBoxC /*?*/ _cachedBindings; ObjectBoxC get C => _cachedBindings ??= loadObjectBoxLib(); + +/// Init DartAPI in C for async callbacks. +/// +/// Call each time you're assign a native listener - will throw if the Dart +/// native API isn't available. +/// See https://github.com/objectbox/objectbox-dart/issues/143 +void initializeDartAPI() { + if (_dartAPIinitialized == null) { + final errCode = C.dartc_init_api(NativeApi.initializeApiDLData); + _dartAPIinitialized = (OBX_SUCCESS == errCode); + if (!_dartAPIinitialized) { + _dartAPIinitException = latestNativeError( + codeIfMissing: errCode, + dartMsg: "Dart/Flutter SDK you're using is not compatible with " + 'ObjectBox observers, query streams and Sync event streams. ' + 'Please consider using Flutter v1.20.x or v1.22.x (or Dart v2.10.x). ' + 'See https://github.com/objectbox/objectbox-dart/issues/197 for more details. ' + 'Native exception'); + } + } + + if (_dartAPIinitException != null) { + throw _dartAPIinitException; + } +} + +// null => not initialized +// true => initialized successfully +// false => failed to initialize - incompatible Dart version +bool /*?*/ _dartAPIinitialized; +ObjectBoxException /*?*/ _dartAPIinitException; diff --git a/objectbox/lib/src/bindings/objectbox-c.dart b/objectbox/lib/src/bindings/objectbox-c.dart index bb61d1819..0dc18c1c6 100644 --- a/objectbox/lib/src/bindings/objectbox-c.dart +++ b/objectbox/lib/src/bindings/objectbox-c.dart @@ -49,7 +49,8 @@ class ObjectBoxC { _dart_version_is_at_least _version_is_at_least; /// /// Return the version of the library to be printed. - /// /// The format may change; to query for version use the int based methods instead. + /// /// The format may change in any future release; only use for information purposes. + /// /// @see obx_version() and obx_version_is_at_least() ffi.Pointer version_string() { _version_string ??= _dylib.lookupFunction<_c_version_string, _dart_version_string>( @@ -60,7 +61,8 @@ class ObjectBoxC { _dart_version_string _version_string; /// /// Return the version of the ObjectBox core to be printed. - /// /// The format may change, do not rely on its current form. + /// /// The format may change in any future release; only use for information purposes. + /// /// @see obx_version() and obx_version_is_at_least() ffi.Pointer version_core_string() { _version_core_string ??= _dylib.lookupFunction<_c_version_core_string, _dart_version_core_string>('obx_version_core_string'); @@ -69,21 +71,22 @@ class ObjectBoxC { _dart_version_core_string _version_core_string; - /// /// Delete the store files from the given directory - int remove_db_files( - ffi.Pointer directory, + /// /// Checks whether the given feature is available in the currently loaded library. + bool has_feature( + int feature, ) { - _remove_db_files ??= - _dylib.lookupFunction<_c_remove_db_files, _dart_remove_db_files>( - 'obx_remove_db_files'); - return _remove_db_files( - directory, - ); + _has_feature ??= _dylib + .lookupFunction<_c_has_feature, _dart_has_feature>('obx_has_feature'); + return _has_feature( + feature, + ) != + 0; } - _dart_remove_db_files _remove_db_files; + _dart_has_feature _has_feature; /// /// Check whether functions returning OBX_bytes_array are fully supported (depends on build, invariant during runtime) + /// /// @deprecated use obx_has_feature(OBXFeature_BytesArray) instead bool supports_bytes_array() { _supports_bytes_array ??= _dylib.lookupFunction<_c_supports_bytes_array, _dart_supports_bytes_array>('obx_supports_bytes_array'); @@ -93,6 +96,7 @@ class ObjectBoxC { _dart_supports_bytes_array _supports_bytes_array; /// /// Check whether time series functions are available in the version of this library + /// /// @deprecated use obx_has_feature(OBXFeature_TimeSeries) instead bool supports_time_series() { _supports_time_series ??= _dylib.lookupFunction<_c_supports_time_series, _dart_supports_time_series>('obx_supports_time_series'); @@ -101,6 +105,20 @@ class ObjectBoxC { _dart_supports_time_series _supports_time_series; + /// /// Delete the store files from the given directory + int remove_db_files( + ffi.Pointer directory, + ) { + _remove_db_files ??= + _dylib.lookupFunction<_c_remove_db_files, _dart_remove_db_files>( + 'obx_remove_db_files'); + return _remove_db_files( + directory, + ); + } + + _dart_remove_db_files _remove_db_files; + /// /// Return the error status on the current thread and clear the error state. /// /// The buffer returned in out_message is valid only until the next call into ObjectBox. /// /// @param out_error receives the error code; optional: may be NULL @@ -4737,6 +4755,7 @@ class ObjectBoxC { /// /// Before calling any of the other sync APIs, ensure that those are actually available. /// /// If the application is linked a non-sync ObjectBox library, this allows you to fail gracefully. /// /// @return true if this library comes with the sync API + /// /// @deprecated use obx_has_feature(OBXFeature_Sync) bool sync_available() { _sync_available ??= _dylib.lookupFunction<_c_sync_available, _dart_sync_available>( @@ -4780,7 +4799,7 @@ class ObjectBoxC { /// /// Sets credentials to authenticate the client with the server. /// /// See OBXSyncCredentialsType for available options. /// /// The accepted OBXSyncCredentials type depends on your sync-server configuration. - /// /// @param data may be NULL, i.e. in combination with OBXSyncCredentialsType_UNCHECKED + /// /// @param data may be NULL, i.e. in combination with OBXSyncCredentialsType_NONE int sync_credentials( ffi.Pointer sync_1, int type, @@ -5095,6 +5114,186 @@ class ObjectBoxC { } _dart_sync_listener_change _sync_listener_change; + + /// /// Initializes Dart API - call before any other obx_dart_* functions. + int dartc_init_api( + ffi.Pointer data, + ) { + _dartc_init_api ??= + _dylib.lookupFunction<_c_dartc_init_api, _dart_dartc_init_api>( + 'obx_dart_init_api'); + return _dartc_init_api( + data, + ); + } + + _dart_dartc_init_api _dartc_init_api; + + /// /// @see obx_observe() + /// /// Note: use obx_observer_close() to free unassign the observer and free resources after you're done with it + ffi.Pointer dartc_observe( + ffi.Pointer store, + int native_port, + ) { + _dartc_observe ??= + _dylib.lookupFunction<_c_dartc_observe, _dart_dartc_observe>( + 'obx_dart_observe'); + return _dartc_observe( + store, + native_port, + ); + } + + _dart_dartc_observe _dartc_observe; + + ffi.Pointer dartc_observe_single_type( + ffi.Pointer store, + int type_id, + int native_port, + ) { + _dartc_observe_single_type ??= _dylib.lookupFunction< + _c_dartc_observe_single_type, + _dart_dartc_observe_single_type>('obx_dart_observe_single_type'); + return _dartc_observe_single_type( + store, + type_id, + native_port, + ); + } + + _dart_dartc_observe_single_type _dartc_observe_single_type; + + /// /// @param listener may be NULL + int dartc_sync_listener_close( + ffi.Pointer listener, + ) { + _dartc_sync_listener_close ??= _dylib.lookupFunction< + _c_dartc_sync_listener_close, + _dart_dartc_sync_listener_close>('obx_dart_sync_listener_close'); + return _dartc_sync_listener_close( + listener, + ); + } + + _dart_dartc_sync_listener_close _dartc_sync_listener_close; + + ffi.Pointer dartc_sync_listener_connect( + ffi.Pointer sync_1, + int native_port, + ) { + _dartc_sync_listener_connect ??= _dylib.lookupFunction< + _c_dartc_sync_listener_connect, + _dart_dartc_sync_listener_connect>('obx_dart_sync_listener_connect'); + return _dartc_sync_listener_connect( + sync_1, + native_port, + ); + } + + _dart_dartc_sync_listener_connect _dartc_sync_listener_connect; + + /// /// @see obx_sync_listener_disconnect() + ffi.Pointer dartc_sync_listener_disconnect( + ffi.Pointer sync_1, + int native_port, + ) { + _dartc_sync_listener_disconnect ??= _dylib.lookupFunction< + _c_dartc_sync_listener_disconnect, + _dart_dartc_sync_listener_disconnect>( + 'obx_dart_sync_listener_disconnect'); + return _dartc_sync_listener_disconnect( + sync_1, + native_port, + ); + } + + _dart_dartc_sync_listener_disconnect _dartc_sync_listener_disconnect; + + /// /// @see obx_sync_listener_login() + ffi.Pointer dartc_sync_listener_login( + ffi.Pointer sync_1, + int native_port, + ) { + _dartc_sync_listener_login ??= _dylib.lookupFunction< + _c_dartc_sync_listener_login, + _dart_dartc_sync_listener_login>('obx_dart_sync_listener_login'); + return _dartc_sync_listener_login( + sync_1, + native_port, + ); + } + + _dart_dartc_sync_listener_login _dartc_sync_listener_login; + + /// /// @see obx_sync_listener_login_failure() + ffi.Pointer dartc_sync_listener_login_failure( + ffi.Pointer sync_1, + int native_port, + ) { + _dartc_sync_listener_login_failure ??= _dylib.lookupFunction< + _c_dartc_sync_listener_login_failure, + _dart_dartc_sync_listener_login_failure>( + 'obx_dart_sync_listener_login_failure'); + return _dartc_sync_listener_login_failure( + sync_1, + native_port, + ); + } + + _dart_dartc_sync_listener_login_failure _dartc_sync_listener_login_failure; + + /// /// @see obx_sync_listener_complete() + ffi.Pointer dartc_sync_listener_complete( + ffi.Pointer sync_1, + int native_port, + ) { + _dartc_sync_listener_complete ??= _dylib.lookupFunction< + _c_dartc_sync_listener_complete, + _dart_dartc_sync_listener_complete>('obx_dart_sync_listener_complete'); + return _dartc_sync_listener_complete( + sync_1, + native_port, + ); + } + + _dart_dartc_sync_listener_complete _dartc_sync_listener_complete; + + /// /// @see obx_sync_listener_change() + ffi.Pointer dartc_sync_listener_change( + ffi.Pointer sync_1, + int native_port, + ) { + _dartc_sync_listener_change ??= _dylib.lookupFunction< + _c_dartc_sync_listener_change, + _dart_dartc_sync_listener_change>('obx_dart_sync_listener_change'); + return _dartc_sync_listener_change( + sync_1, + native_port, + ); + } + + _dart_dartc_sync_listener_change _dartc_sync_listener_change; +} + +abstract class OBXFeature { + /// /// Functions that are returning multiple results (e.g. multiple objects) can be only used if this is available. + /// /// This is only available for 64-bit OSes and is the opposite of "chunked mode", which forces to consume results + /// /// in chunks (e.g. one by one). + /// /// Since chunked mode consumes a bit less RAM, ResultArray style functions are typically only preferable if + /// /// there's an additional overhead per call, e.g. caused by a higher level language abstraction like CGo. + static const int ResultArray = 1; + + /// /// TimeSeries support (date/date-nano companion ID and other time-series functionality). + static const int TimeSeries = 2; + + /// /// Sync client availability. Visit https://objectbox.io/sync for more details. + static const int Sync = 3; + + /// /// Check whether debug log can be enabled during runtime. + static const int DebugLog = 4; + + /// /// HTTP server with a database browser. + static const int ObjectBrowser = 5; } abstract class OBXPropertyType { @@ -5139,6 +5338,15 @@ abstract class OBXEntityFlags { /// /// Enable "data synchronization" for this entity type: objects will be synced with other stores over the network. /// /// It's possible to have local-only (non-synced) types and synced types in the same store (schema/data model). static const int SYNC_ENABLED = 2; + + /// /// Makes object IDs for a synced types (SYNC_ENABLED is set) global. + /// /// By default (not using this flag), the 64 bit object IDs have a local scope and are not unique globally. + /// /// This flag tells ObjectBox to treat object IDs globally and thus no ID mapping (local <-> global) is performed. + /// /// Often this is used with assignable IDs (ID_SELF_ASSIGNABLE property flag is set) and some special ID scheme. + /// /// Note: typically you won't do this with automatically assigned IDs, set by the local ObjectBox store. + /// /// Two devices would likely overwrite each other's object during sync as object IDs are prone to collide. + /// /// It might be OK if you can somehow ensure that only a single device will create new IDs. + static const int SHARED_GLOBAL_IDS = 4; } /// /// Bit-flags defining the behavior of properties. @@ -5453,6 +5661,8 @@ class OBX_sync_change_array extends ffi.Struct { int count; } +class OBX_dart_sync_listener extends ffi.Struct {} + const int OBX_VERSION_MAJOR = 0; const int OBX_VERSION_MINOR = 11; @@ -5527,6 +5737,10 @@ const int OBX_ERROR_FILE_PAGES_CORRUPT = 10503; const int OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND = 10504; +const int OBX_ERROR_TIME_SERIES_NOT_AVAILABLE = 10601; + +const int OBX_ERROR_SYNC_NOT_AVAILABLE = 10602; + typedef _c_version = ffi.Void Function( ffi.Pointer major, ffi.Pointer minor, @@ -5559,12 +5773,12 @@ typedef _c_version_core_string = ffi.Pointer Function(); typedef _dart_version_core_string = ffi.Pointer Function(); -typedef _c_remove_db_files = ffi.Int32 Function( - ffi.Pointer directory, +typedef _c_has_feature = ffi.Uint8 Function( + ffi.Int32 feature, ); -typedef _dart_remove_db_files = int Function( - ffi.Pointer directory, +typedef _dart_has_feature = int Function( + int feature, ); typedef _c_supports_bytes_array = ffi.Uint8 Function(); @@ -5575,6 +5789,14 @@ typedef _c_supports_time_series = ffi.Uint8 Function(); typedef _dart_supports_time_series = int Function(); +typedef _c_remove_db_files = ffi.Int32 Function( + ffi.Pointer directory, +); + +typedef _dart_remove_db_files = int Function( + ffi.Pointer directory, +); + typedef _c_last_error_pop = ffi.Uint8 Function( ffi.Pointer out_error, ffi.Pointer> out_message, @@ -8696,3 +8918,113 @@ typedef _dart_sync_listener_change = void Function( ffi.Pointer> listener, ffi.Pointer listener_arg, ); + +typedef _c_dartc_init_api = ffi.Int32 Function( + ffi.Pointer data, +); + +typedef _dart_dartc_init_api = int Function( + ffi.Pointer data, +); + +typedef _c_dartc_observe = ffi.Pointer Function( + ffi.Pointer store, + ffi.Int64 native_port, +); + +typedef _dart_dartc_observe = ffi.Pointer Function( + ffi.Pointer store, + int native_port, +); + +typedef _c_dartc_observe_single_type = ffi.Pointer Function( + ffi.Pointer store, + ffi.Uint32 type_id, + ffi.Int64 native_port, +); + +typedef _dart_dartc_observe_single_type = ffi.Pointer Function( + ffi.Pointer store, + int type_id, + int native_port, +); + +typedef _c_dartc_sync_listener_close = ffi.Int32 Function( + ffi.Pointer listener, +); + +typedef _dart_dartc_sync_listener_close = int Function( + ffi.Pointer listener, +); + +typedef _c_dartc_sync_listener_connect = ffi.Pointer + Function( + ffi.Pointer sync_1, + ffi.Int64 native_port, +); + +typedef _dart_dartc_sync_listener_connect = ffi.Pointer + Function( + ffi.Pointer sync_1, + int native_port, +); + +typedef _c_dartc_sync_listener_disconnect = ffi.Pointer + Function( + ffi.Pointer sync_1, + ffi.Int64 native_port, +); + +typedef _dart_dartc_sync_listener_disconnect + = ffi.Pointer Function( + ffi.Pointer sync_1, + int native_port, +); + +typedef _c_dartc_sync_listener_login = ffi.Pointer + Function( + ffi.Pointer sync_1, + ffi.Int64 native_port, +); + +typedef _dart_dartc_sync_listener_login = ffi.Pointer + Function( + ffi.Pointer sync_1, + int native_port, +); + +typedef _c_dartc_sync_listener_login_failure + = ffi.Pointer Function( + ffi.Pointer sync_1, + ffi.Int64 native_port, +); + +typedef _dart_dartc_sync_listener_login_failure + = ffi.Pointer Function( + ffi.Pointer sync_1, + int native_port, +); + +typedef _c_dartc_sync_listener_complete = ffi.Pointer + Function( + ffi.Pointer sync_1, + ffi.Int64 native_port, +); + +typedef _dart_dartc_sync_listener_complete = ffi.Pointer + Function( + ffi.Pointer sync_1, + int native_port, +); + +typedef _c_dartc_sync_listener_change = ffi.Pointer + Function( + ffi.Pointer sync_1, + ffi.Int64 native_port, +); + +typedef _dart_dartc_sync_listener_change = ffi.Pointer + Function( + ffi.Pointer sync_1, + int native_port, +); diff --git a/objectbox/lib/src/bindings/objectbox-dart.h b/objectbox/lib/src/bindings/objectbox-dart.h new file mode 100644 index 000000000..f9a95b4e7 --- /dev/null +++ b/objectbox/lib/src/bindings/objectbox-dart.h @@ -0,0 +1,75 @@ +/* + * Copyright 2021 ObjectBox Ltd. All rights reserved. + * + * 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. + */ + +#ifndef OBJECTBOX_DART_H +#define OBJECTBOX_DART_H + +#include + +#include "objectbox.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//---------------------------------------------- +// Dart-specific binding +// +// Following section provides [Dart](https://dart.dev) specific async callbacks integration. +// These functions are only used internally by [objectbox-dart](https://github.com/objectbox/objectbox-dart) binding. +// In short - instead of issuing callbacks from background threads, their messages are sent to Dart over NativePorts. +//---------------------------------------------- + +/// Initializes Dart API - call before any other obx_dart_* functions. +obx_err obx_dart_init_api(void* data); + +/// @see obx_observe() +/// Note: use obx_observer_close() to free unassign the observer and free resources after you're done with it +OBX_observer* obx_dart_observe(OBX_store* store, int64_t native_port); + +// @see obx_observe_single_type() +OBX_observer* obx_dart_observe_single_type(OBX_store* store, obx_schema_id type_id, int64_t native_port); + +// Note: use OBX_dart_sync_listener_close() to unassign the listener and free native resources +struct OBX_dart_sync_listener; +typedef struct OBX_dart_sync_listener OBX_dart_sync_listener; + +/// @param listener may be NULL +obx_err obx_dart_sync_listener_close(OBX_dart_sync_listener* listener); + +// @see obx_sync_listener_connect() +OBX_dart_sync_listener* obx_dart_sync_listener_connect(OBX_sync* sync, int64_t native_port); + +/// @see obx_sync_listener_disconnect() +OBX_dart_sync_listener* obx_dart_sync_listener_disconnect(OBX_sync* sync, int64_t native_port); + +/// @see obx_sync_listener_login() +OBX_dart_sync_listener* obx_dart_sync_listener_login(OBX_sync* sync, int64_t native_port); + +/// @see obx_sync_listener_login_failure() +OBX_dart_sync_listener* obx_dart_sync_listener_login_failure(OBX_sync* sync, int64_t native_port); + +/// @see obx_sync_listener_complete() +OBX_dart_sync_listener* obx_dart_sync_listener_complete(OBX_sync* sync, int64_t native_port); + +/// @see obx_sync_listener_change() +OBX_dart_sync_listener* obx_dart_sync_listener_change(OBX_sync* sync, int64_t native_port); + +#ifdef __cplusplus +} +#endif + +#endif // OBJECTBOX_DART_H diff --git a/objectbox/lib/src/bindings/objectbox.h b/objectbox/lib/src/bindings/objectbox.h index f391e4c4d..761901acc 100644 --- a/objectbox/lib/src/bindings/objectbox.h +++ b/objectbox/lib/src/bindings/objectbox.h @@ -72,7 +72,10 @@ typedef int obx_err; typedef bool obx_data_visitor(void* user_data, const void* data, size_t size); //---------------------------------------------- -// Utilities +// Runtime library information +// +// Functions in this group provide information about the loaded ObjectBox library. +// Their return values are invariable during runtime - they depend solely on the loaded library and its build settings. //---------------------------------------------- /// Return the version of the library as ints. Pointers may be null @@ -82,25 +85,57 @@ void obx_version(int* major, int* minor, int* patch); bool obx_version_is_at_least(int major, int minor, int patch); /// Return the version of the library to be printed. -/// The format may change; to query for version use the int based methods instead. +/// The format may change in any future release; only use for information purposes. +/// @see obx_version() and obx_version_is_at_least() const char* obx_version_string(void); /// Return the version of the ObjectBox core to be printed. -/// The format may change, do not rely on its current form. +/// The format may change in any future release; only use for information purposes. +/// @see obx_version() and obx_version_is_at_least() const char* obx_version_core_string(void); -/// To be used for putting objects with prepared ID slots, e.g. obx_cursor_put_object(). -#define OBX_ID_NEW 0xFFFFFFFFFFFFFFFF +typedef enum { + /// Functions that are returning multiple results (e.g. multiple objects) can be only used if this is available. + /// This is only available for 64-bit OSes and is the opposite of "chunked mode", which forces to consume results + /// in chunks (e.g. one by one). + /// Since chunked mode consumes a bit less RAM, ResultArray style functions are typically only preferable if + /// there's an additional overhead per call, e.g. caused by a higher level language abstraction like CGo. + OBXFeature_ResultArray = 1, -/// Delete the store files from the given directory -obx_err obx_remove_db_files(char const* directory); + /// TimeSeries support (date/date-nano companion ID and other time-series functionality). + OBXFeature_TimeSeries = 2, + + /// Sync client availability. Visit https://objectbox.io/sync for more details. + OBXFeature_Sync = 3, + + /// Check whether debug log can be enabled during runtime. + OBXFeature_DebugLog = 4, + + /// HTTP server with a database browser. + OBXFeature_ObjectBrowser = 5, +} OBXFeature; + +/// Checks whether the given feature is available in the currently loaded library. +bool obx_has_feature(OBXFeature feature); /// Check whether functions returning OBX_bytes_array are fully supported (depends on build, invariant during runtime) +/// @deprecated use obx_has_feature(OBXFeature_BytesArray) instead bool obx_supports_bytes_array(void); /// Check whether time series functions are available in the version of this library +/// @deprecated use obx_has_feature(OBXFeature_TimeSeries) instead bool obx_supports_time_series(void); +//---------------------------------------------- +// Utilities +//---------------------------------------------- + +/// To be used for putting objects with prepared ID slots, e.g. obx_cursor_put_object(). +#define OBX_ID_NEW 0xFFFFFFFFFFFFFFFF + +/// Delete the store files from the given directory +obx_err obx_remove_db_files(char const* directory); + //---------------------------------------------- // Return codes //---------------------------------------------- @@ -165,6 +200,10 @@ bool obx_supports_time_series(void); /// A requested schema object (e.g., an entity or a property) was not found in the schema #define OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND 10504 +/// Feature specific errors +#define OBX_ERROR_TIME_SERIES_NOT_AVAILABLE 10601 +#define OBX_ERROR_SYNC_NOT_AVAILABLE 10602 + //---------------------------------------------- // Error info; obx_last_error_* //---------------------------------------------- @@ -227,6 +266,15 @@ typedef enum { /// Enable "data synchronization" for this entity type: objects will be synced with other stores over the network. /// It's possible to have local-only (non-synced) types and synced types in the same store (schema/data model). OBXEntityFlags_SYNC_ENABLED = 2, + + /// Makes object IDs for a synced types (SYNC_ENABLED is set) global. + /// By default (not using this flag), the 64 bit object IDs have a local scope and are not unique globally. + /// This flag tells ObjectBox to treat object IDs globally and thus no ID mapping (local <-> global) is performed. + /// Often this is used with assignable IDs (ID_SELF_ASSIGNABLE property flag is set) and some special ID scheme. + /// Note: typically you won't do this with automatically assigned IDs, set by the local ObjectBox store. + /// Two devices would likely overwrite each other's object during sync as object IDs are prone to collide. + /// It might be OK if you can somehow ensure that only a single device will create new IDs. + OBXEntityFlags_SHARED_GLOBAL_IDS = 4, } OBXEntityFlags; /// Bit-flags defining the behavior of properties. @@ -1640,6 +1688,7 @@ void obx_posix_sem_prefix_set(const char* prefix); /// Before calling any of the other sync APIs, ensure that those are actually available. /// If the application is linked a non-sync ObjectBox library, this allows you to fail gracefully. /// @return true if this library comes with the sync API +/// @deprecated use obx_has_feature(OBXFeature_Sync) bool obx_sync_available(); struct OBX_sync; @@ -1734,7 +1783,7 @@ obx_err obx_sync_close(OBX_sync* sync); /// Sets credentials to authenticate the client with the server. /// See OBXSyncCredentialsType for available options. /// The accepted OBXSyncCredentials type depends on your sync-server configuration. -/// @param data may be NULL, i.e. in combination with OBXSyncCredentialsType_UNCHECKED +/// @param data may be NULL, i.e. in combination with OBXSyncCredentialsType_NONE obx_err obx_sync_credentials(OBX_sync* sync, OBXSyncCredentialsType type, const void* data, size_t size); /// Configures the maximum number of outgoing TX messages that can be sent without an ACK from the server. diff --git a/objectbox/lib/src/box.dart b/objectbox/lib/src/box.dart index 49072063e..06ce3c61b 100644 --- a/objectbox/lib/src/box.dart +++ b/objectbox/lib/src/box.dart @@ -51,7 +51,7 @@ class Box { .any((ModelProperty prop) => prop.isRelation), _hasToManyRelations = _entity.model.relations.isNotEmpty || _entity.model.backlinks.isNotEmpty, - _cBox = C.box(_store.ptr, _entity.model.id.id) { + _cBox = C.box(InternalStoreAccess.ptr(_store), _entity.model.id.id) { checkObxPtr(_cBox, 'failed to create box'); } diff --git a/objectbox/lib/src/observable.dart b/objectbox/lib/src/observable.dart index 2f229f095..4d2c7365d 100644 --- a/objectbox/lib/src/observable.dart +++ b/objectbox/lib/src/observable.dart @@ -1,102 +1,146 @@ import 'dart:async'; import 'dart:ffi'; +import 'dart:isolate'; +import 'dart:typed_data'; import 'bindings/bindings.dart'; +import 'bindings/helpers.dart'; +import 'modelinfo/entity_definition.dart'; import 'query/query.dart'; import 'store.dart'; -import 'util.dart'; - -// ignore_for_file: non_constant_identifier_names - -// dart callback signature -typedef _Any = void Function(Pointer, Pointer, int); - -class _Observable { - static final _anyObserver = >{}; - static final _any = >{}; - - // sync:true -> ObjectBoxException: 10001 TX is not active anymore: #101 - static final controller = StreamController.broadcast(); - - // The user_data is used to pass the store ptr address - // in case there is no consensus on the entity id between stores - static void _anyCallback( - Pointer user_data, Pointer mutated_ids, int mutated_count) { - final storeAddress = user_data.address; - // call schema's callback - final storeCallbacks = _any[storeAddress]; - if (storeCallbacks != null) { - for (var i = 0; i < mutated_count; i++) { - storeCallbacks[mutated_ids[i]] - ?.call(user_data, mutated_ids, mutated_count); - } - } + +/// Simple wrapper used below in ObservableStore to reduce code duplication. +/// Contains shared code for single-entity observer and the generic/global one. +class _Observer { + StreamController /*?*/ controller; + Pointer /*?*/ _cObserver; + ReceivePort /*?*/ receivePort; + + int get nativePort => receivePort /*!*/ .sendPort.nativePort; + + set cObserver(Pointer value) { + _cObserver = checkObxPtr(value, 'observer initialization failed'); + _debugLog('started'); } - static void subscribe(Store store) { - syncOrObserversExclusive.mark(store); + Stream get stream => controller /*!*/ .stream; - final callback = Pointer.fromFunction(_anyCallback); - final storePtr = store.ptr; - _anyObserver[storePtr.address] = - C.observe(storePtr, callback, storePtr.cast()); - InternalStoreAccess.addCloseListener(store, _anyObserver[storePtr.address], - () { - unsubscribe(store); - }); + _Observer() { + initializeDartAPI(); } - // #53 ffi:Pointer finalizer - static void unsubscribe(Store store) { - final storeAddress = store.ptr.address; - if (!_anyObserver.containsKey(storeAddress)) { - return; - } - InternalStoreAccess.removeCloseListener(store, _anyObserver[storeAddress]); - C.observer_close(_anyObserver[storeAddress]); - _anyObserver.remove(storeAddress); - syncOrObserversExclusive.unmark(store); + // start() is called whenever user starts listen()-ing to the stream + void init(void Function() start) { + controller = StreamController( + onListen: start, onPause: stop, onResume: start, onCancel: stop); } - static bool isSubscribed(Store store) => - _Observable._anyObserver.containsKey(store.ptr.address); -} + // stop() is called when the stream subscription is paused or canceled + void stop() { + _debugLog('stopped'); + if (_cObserver != null) { + checkObx(C.observer_close(_cObserver)); + _cObserver = null; + } -/// Streamable adds stream support to queries. The stream reruns the query -/// whenever there's a change in any of the objects in the queried Box -/// (regardless of the filter conditions). -extension Streamable on Query { - void _setup() { - if (!_Observable.isSubscribed(store)) { - _Observable.subscribe(store); + if (receivePort != null) { + receivePort.close(); + receivePort = null; } - final storeAddress = store.ptr.address; + } - _Observable._any[storeAddress] ??= {}; - _Observable._any[storeAddress] /*!*/ [entityId] ??= (u, _, __) { - // dummy value to trigger an event - _Observable.controller.add(entityId); - }; + void _debugLog(String message) { + // print('Observer=${_cObserver?.address} $message'); } +} - /// Create a stream, executing [Query.find()] whenever there's a change to any - /// of the objects in the queried Box. - Stream> findStream( - {@Deprecated('Use offset() instead') int offset = 0, - @Deprecated('Use limit() instead') int limit = 0}) { - _setup(); - return _Observable.controller.stream.where((e) => e == entityId).map((_) { - if (offset != 0) this.offset(offset); - if (limit != 0) this.limit(limit); - return find(); +/// StreamController implementation inspired by the sample controller sample at: +/// https://dart.dev/articles/libraries/creating-streams#honoring-the-pause-state +/// https://dart.dev/articles/libraries/code/stream_controller.dart +extension ObservableStore on Store { + /// Create a stream to data changes on EntityT (stored Entity class). + /// + /// The stream receives an event whenever an object of EntityT is created or + /// changed or deleted. Make sure to cancel() the subscription after you're + /// done with it to avoid hanging change listeners. + Stream subscribe() { + final observer = _Observer(); + final entityId = InternalStoreAccess.entityDef(this).model.id.id; + + observer.init(() { + // We're listening to events on single entity so there's no argument. + // Ideally, controller.add() would work but it doesn't, even though we're + // using StreamController so the argument type is `void`. + observer.receivePort = ReceivePort() + ..listen((dynamic _) => observer.controller.add(null)); + observer.cObserver = C.dartc_observe_single_type( + InternalStoreAccess.ptr(this), entityId, observer.nativePort); }); + + return observer.stream; } - /// Use this for Query Property - Stream> get stream { - _setup(); - return _Observable.controller.stream - .where((e) => e == entityId) - .map((_) => this); + /// Create a stream to data changes on all Entity types. + /// + /// The stream receives an event whenever any data changes in the database. + /// Make sure to cancel() the subscription after you're done with it to avoid + /// hanging change listeners. + Stream subscribeAll() { + initializeDartAPI(); + final observer = _Observer(); + + // create a map from Entity ID to Entity type (dart class) + final entityTypesById = {}; + InternalStoreAccess.defs(this).bindings.forEach( + (Type entity, EntityDefinition entityDef) => + entityTypesById[entityDef.model.id.id] = entity); + + observer.init(() { + // We're listening to a events for all entity types. C-API sends entity ID + // and we must map it to a dart type (class) corresponding to that entity. + observer.receivePort = ReceivePort() + ..listen((dynamic entityIds) { + if (entityIds is! Uint32List) { + observer.controller.addError(Exception( + 'Received invalid data format from the core notification: (${entityIds.runtimeType}) $entityIds')); + return; + } + + (entityIds as Uint32List).forEach((int entityId) { + if (entityId is! int) { + observer.controller.addError(Exception( + 'Received invalid item data format from the core notification: (${entityId.runtimeType}) $entityId')); + return; + } + + final entityType = entityTypesById[entityId]; + if (entityType == null) { + observer.controller.addError(Exception( + 'Received data change notification for an unknown entity ID $entityId')); + } else { + observer.controller.add(entityType); + } + }); + }); + observer.cObserver = + C.dartc_observe(InternalStoreAccess.ptr(this), observer.nativePort); + }); + + return observer.stream; } } + +/// Streamable adds stream support to queries. +extension Streamable on Query { + /// Create a stream, executing [Query.find()] whenever there's a change to any + /// of the objects in the queried Box. + /// TODO consider removing, see issue #195 + Stream> findStream() => stream.map((q) => q.find()); + + /// The stream gets notified whenever there's a change in any of the objects + /// in the queried Box (regardless of the filter conditions). + /// + /// You can use the given [Query] object to run any of its operation, + /// e.g. find(), count(), execute a [property()] query + Stream> get stream => store.subscribe().map((_) => this); +} diff --git a/objectbox/lib/src/query/builder.dart b/objectbox/lib/src/query/builder.dart index de05db4df..659871c5c 100644 --- a/objectbox/lib/src/query/builder.dart +++ b/objectbox/lib/src/query/builder.dart @@ -5,7 +5,11 @@ class QueryBuilder extends _QueryBuilder { /// Start creating a query. QueryBuilder(Store store, EntityDefinition entity, Condition /*?*/ qc) : super( - store, entity, qc, C.query_builder(store.ptr, entity.model.id.id)); + store, + entity, + qc, + C.query_builder( + InternalStoreAccess.ptr(store), entity.model.id.id)); /// Finish building a [Query]. Call [Query.close()] after you're done with it /// to free resources. diff --git a/objectbox/lib/src/store.dart b/objectbox/lib/src/store.dart index 46601d834..162f182fe 100644 --- a/objectbox/lib/src/store.dart +++ b/objectbox/lib/src/store.dart @@ -1,4 +1,6 @@ import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; import 'package:ffi/ffi.dart'; @@ -18,11 +20,17 @@ class Store { /*late final*/ Pointer _cStore; final _boxes = {}; final ModelDefinition _defs; + bool _closed = false; + ByteData _reference; /// A list of observers of the Store.close() event. final _onClose = {}; /// Creates a BoxStore using the model definition from the generated + /// whether this store was created from a pointer (won't close in that case) + final bool _weak; + + /// Creates a BoxStore using the model definition from your /// `objectbox.g.dart` file. /// /// For example in a Flutter app: @@ -42,7 +50,8 @@ class Store { {String /*?*/ directory, int /*?*/ maxDBSizeInKB, int /*?*/ fileMode, - int /*?*/ maxReaders}) { + int /*?*/ maxReaders}) + : _weak = false { var model = Model(_defs.model); var opt = C.opt(); @@ -97,10 +106,79 @@ class Store { } } + /// Create a Dart store instance from an existing native store reference. + /// Use this if you want to access the same store from multiple isolates. + /// This results in two (or more) isolates having access to the same + /// underlying native store. Concurrent access is ensured using implicit or + /// explicit transactions. + /// Note: make sure you don't use store in any of the isolates after the + /// original store is closed (by calling [close()]). + /// + /// To do this, you'd send the [reference] over a [SendPort], receive + /// it in another isolate and pass it to [attach()]. + /// + /// Example (see test/isolates_test.dart for an actual working example) + /// ```dart + /// // Main isolate: + /// final store = Store(getObjectBoxModel()) + /// + /// ... + /// + /// // use the sendPort of another isolate to send an open store reference. + /// sendPort.send(store.reference); + /// + /// ... + /// + /// // receive the reference in another isolate + /// Store store; + /// // Listen for messages + /// await for (final msg in port) { + /// if (store == null) { + /// // first message data is existing Store's reference + /// store = Store.attach(getObjectBoxModel(), msg); + /// } + /// ... + /// } + /// ``` + Store.fromReference(this._defs, this._reference) + : _weak = true // must not close the same native store twice + { + // see [reference] for serialization order + final readPid = _reference.getUint64(0 * _int64Size); + if (readPid != pid) { + throw ArgumentError("Reference.processId $readPid doesn't match the " + 'current process PID $pid'); + } + + _cStore = Pointer.fromAddress(_reference.getUint64(1 * _int64Size)); + if (_cStore.address == 0) { + throw ArgumentError.value(_cStore.address, 'reference.nativePointer', + 'Given native pointer is empty'); + } + } + + /// Returns a store reference you can use to create a new store instance with + /// a single underlying native store. See [Store.attach()] for more details. + ByteData get reference { + if (_reference == null) { + _reference = ByteData(2 * _int64Size); + + // Ensure we only try to access the store created in the same process. + // Also serves as a simple sanity check/hash. + _reference.setUint64(0 * _int64Size, pid); + + _reference.setUint64(1 * _int64Size, _ptr.address); + } + return _reference; + } + /// Closes this store. /// /// Don't try to call any other ObjectBox methods after the store is closed. void close() { + if (_closed) return; + _closed = true; + _boxes.values.forEach(InternalBoxAccess.close); _boxes.clear(); @@ -109,7 +187,7 @@ class Store { _onClose.values.toList(growable: false).forEach((listener) => listener()); _onClose.clear(); - checkObx(C.store_close(_cStore)); + if (!_weak) checkObx(C.store_close(_cStore)); } /// Returns a cached Box instance. @@ -145,7 +223,10 @@ class Store { SyncClient /*?*/ syncClient() => syncClientsStorage[this]; /// The low-level pointer to this store. - Pointer get ptr => _cStore; + Pointer get _ptr { + if (_closed) throw Exception('Cannot access a closed store pointer'); + return _cStore; + } } /// Internal only. @@ -155,6 +236,9 @@ class InternalStoreAccess { /// Access entity model for the given class (Dart Type). static EntityDefinition entityDef(Store store) => store._entityDef(); + /// Access model definitions + static ModelDefinition defs(Store store) => store._defs; + /// Adds a listener to the [store.close()] event. static void addCloseListener( Store store, dynamic key, void Function() listener) => @@ -163,4 +247,9 @@ class InternalStoreAccess { /// Removes a [store.close()] event listener. static void removeCloseListener(Store store, dynamic key) => store._onClose.remove(key); + + /// The low-level pointer to this store. + static Pointer ptr(Store store) => store._ptr; } + +const _int64Size = 8; diff --git a/objectbox/lib/src/sync.dart b/objectbox/lib/src/sync.dart index 0095fd8c8..1eb47b53c 100644 --- a/objectbox/lib/src/sync.dart +++ b/objectbox/lib/src/sync.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:convert' show utf8; import 'dart:ffi'; -import 'dart:typed_data' show Uint8List; +import 'dart:isolate'; +import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart'; @@ -8,6 +10,7 @@ import 'package:meta/meta.dart'; import 'bindings/bindings.dart'; import 'bindings/helpers.dart'; import 'bindings/structs.dart'; +import 'modelinfo/entity_definition.dart'; import 'store.dart'; import 'util.dart'; @@ -84,6 +87,46 @@ enum SyncRequestUpdatesMode { autoNoPushes } +/// Connection state change event. +enum SyncConnectionEvent { + /// Connection to the server is established. + connected, + + /// Connection to the server is lost. + disconnected +} + +/// Login state change event. +enum SyncLoginEvent { + /// Client has successfully logged in to the server. + loggedIn, + + /// Client's credentials has been rejectd by the server. + /// Connection will NOT be retried until new credentials are provided. + credentialsRejected, + + /// An unknown error occured during authentication. + unknownError +} + +/// Sync incoming data event. +class SyncChange { + /// Entity ID this change relates to. + final int entityId; + + /// Entity type this change relates to. + /// TODO Maybe use SyncChange instead? + final Type entity; + + /// List of "put" (inserted/updated) object IDs. + final List puts; + + /// List of removed object IDs. + final List removals; + + SyncChange._(this.entityId, this.entity, this.puts, this.removals); +} + /// Sync client is used to connect to an ObjectBox sync server. class SyncClient { final Store _store; @@ -108,7 +151,8 @@ class SyncClient { final cServerUri = Utf8.toUtf8(serverUri).cast(); try { _cSync = checkObxPtr( - C.sync_1(_store.ptr, cServerUri), 'failed to create sync client'); + C.sync_1(InternalStoreAccess.ptr(_store), cServerUri), + 'failed to create sync client'); } finally { free(cServerUri); } @@ -120,11 +164,14 @@ class SyncClient { /// It can no longer be used afterwards, make a new sync client instead. /// Does nothing if this sync client has already been closed. void close() { + _connectionEvents?._stop(); + _loginEvents?._stop(); + _completionEvents?._stop(); + _changeEvents?._stop(); final err = C.sync_close(_cSync); _cSync = nullptr; syncClientsStorage.remove(_store); InternalStoreAccess.removeCloseListener(_store, this); - syncOrObserversExclusive.unmark(_store); checkObx(err); } @@ -235,6 +282,271 @@ class SyncClient { free(count); } } + + _SyncListenerGroup /*?*/ _connectionEvents; + + /// Get a broadcast stream of connection state changes (connect/disconnect). + /// + /// Subscribe (listen) to the stream to actually start listening to events. + Stream get connectionEvents { + if (_connectionEvents == null) { + // Combine events from two C listeners: connect & disconnect. + _connectionEvents = + _SyncListenerGroup('sync-connection'); + + _connectionEvents.add(_SyncListenerConfig( + (int nativePort) => C.dartc_sync_listener_connect(ptr, nativePort), + (dynamic _, controller) => + controller.add(SyncConnectionEvent.connected))); + + _connectionEvents.add(_SyncListenerConfig( + (int nativePort) => C.dartc_sync_listener_disconnect(ptr, nativePort), + (dynamic _, controller) => + controller.add(SyncConnectionEvent.disconnected))); + + _connectionEvents.finish(); + } + return _connectionEvents.stream; + } + + _SyncListenerGroup /*?*/ _loginEvents; + + /// Get a broadcast stream of login events (success/failure). + /// + /// Subscribe (listen) to the stream to actually start listening to events. + Stream get loginEvents { + if (_loginEvents == null) { + // Combine events from two C listeners: login & login-failure. + _loginEvents = _SyncListenerGroup('sync-login'); + + _loginEvents.add(_SyncListenerConfig( + (int nativePort) => C.dartc_sync_listener_login(ptr, nativePort), + (dynamic _, controller) => controller.add(SyncLoginEvent.loggedIn))); + + _loginEvents.add(_SyncListenerConfig( + (int nativePort) => + C.dartc_sync_listener_login_failure(ptr, nativePort), + (dynamic code, controller) { + // see OBXSyncCode - TODO should we match any other codes? + switch (code as int) { + case OBXSyncCode.CREDENTIALS_REJECTED: + return controller.add(SyncLoginEvent.credentialsRejected); + default: + return controller.add(SyncLoginEvent.unknownError); + } + })); + + _loginEvents.finish(); + } + return _loginEvents.stream; + } + + _SyncListenerGroup /*?*/ _completionEvents; + + /// Get a broadcast stream of sync completion events - when synchronization + /// of incoming changes has completed. + /// + /// Subscribe (listen) to the stream to actually start listening to events. + Stream get completionEvents { + if (_completionEvents == null) { + _completionEvents = _SyncListenerGroup('sync-completion'); + + _completionEvents.add(_SyncListenerConfig( + (int nativePort) => C.dartc_sync_listener_complete(ptr, nativePort), + (dynamic _, controller) => controller.add(null))); + + _completionEvents.finish(); + } + return _completionEvents.stream; + } + + _SyncListenerGroup> /*?*/ _changeEvents; + + /// Get a broadcast stream of incoming synced data changes. + /// + /// Subscribe (listen) to the stream to actually start listening to events. + Stream> get changeEvents { + if (_changeEvents == null) { + // This stream combines events from two C listeners: connect & disconnect. + _changeEvents = _SyncListenerGroup>('sync-change'); + + // create a map from Entity ID to Entity type (dart class) + final entityTypesById = {}; + InternalStoreAccess.defs(_store).bindings.forEach( + (Type entity, EntityDefinition entityDef) => + entityTypesById[entityDef.model.id.id] = entity); + + _changeEvents.add(_SyncListenerConfig( + (int nativePort) => C.dartc_sync_listener_change(ptr, nativePort), + (dynamic msg, controller) { + if (msg is! List) { + controller.addError(Exception( + 'Received invalid data type from the core notification: (${msg.runtimeType}) $msg')); + return; + } + + final syncChanges = msg as List; + + // List is flattened to List, with SyncChange object + // properties always coming in groups of three (entityId, puts, removals) + const numProperties = 3; + if (syncChanges.length % numProperties != 0) { + controller.addError(Exception( + 'Received invalid list length from the core notification: (${syncChanges.runtimeType}) $syncChanges')); + return; + } + + final changes = []; + for (var i = 0; i < syncChanges.length / numProperties; i++) { + final dynamic entityId = syncChanges[i * numProperties + 0]; + final dynamic putsBytes = syncChanges[i * numProperties + 1]; + final dynamic removalsBytes = syncChanges[i * numProperties + 2]; + + final entityType = entityTypesById[entityId]; + if (entityType == null) { + controller.addError(Exception( + 'Received sync change notification for an unknown entity ID $entityId')); + return; + } + + if (entityId is! int || + putsBytes is! Uint8List || + removalsBytes is! Uint8List) { + controller.addError(Exception( + 'Received invalid list items format from the core notification at i=$i: ' + 'entityId = (${entityId.runtimeType}) $entityId; ' + 'putsBytes = (${putsBytes.runtimeType}) $putsBytes; ' + 'removalsBytes = (${removalsBytes.runtimeType}) $removalsBytes')); + return; + } + + changes.add(SyncChange._( + entityId as int, + entityType, + Uint64List.view((putsBytes as Uint8List).buffer).toList(), + Uint64List.view((removalsBytes as Uint8List).buffer).toList())); + } + + controller.add(changes); + })); + + _changeEvents.finish(); + } + return _changeEvents.stream; + } +} + +/// Configuration for _SyncListenerGroup, setting up a single native listener. +class _SyncListenerConfig { + /// Function to create a new native listener. + final Pointer Function(int nativePort) cListenerInit; + + /// Called on message from a native listener. + final void Function(dynamic msg, StreamController controller) dartListener; + + _SyncListenerConfig(this.cListenerInit, this.dartListener); +} + +/// Wrapper used in SyncClient for event listeners forwarding. +/// Supports merging events from multiple native listeners to a single stream. +class _SyncListenerGroup { + final String name; + bool finished = false; + + /*late final*/ + StreamController controller; + final _configs = <_SyncListenerConfig>[]; + + // currently active native listeners and ports attached to them + final _cListeners = >[]; + final _receivePorts = []; + + Stream get stream { + assert(finished, 'Call finish() before accessing .stream'); + return controller.stream; + } + + /// start() is called whenever user starts listen()-ing to the stream + _SyncListenerGroup(this.name) { + initializeDartAPI(); + } + + /// Add a native->dart forwarder config to the group. + void add(_SyncListenerConfig config) { + assert(!finished, "Can't add more listeners after calling finish()."); + _configs.add(config); + } + + /// Finish the group, creating a listener. + Stream finish() { + assert(!finished, 'finish() may only be called once.'); + controller = StreamController.broadcast( + onListen: _start, + /* not for broadcast streams: onPause: _stop, onResume: _start,*/ + onCancel: _stop); + finished = true; + return controller.stream; + } + + // start() is called when the stream subscription is started or resumed + void _start() { + _debugLog('starting'); + assert(finished, 'Starting an unfinished group?!'); + + var hasError = false; + _configs.forEach((_SyncListenerConfig config) { + if (hasError) return; + + // Initialize a receive port where the native listener will post messages. + final receivePort = ReceivePort() + ..listen((dynamic msg) => config.dartListener(msg, controller)); + + // Store the ReceivePort to be able to close it in _stop(). + _receivePorts.add(receivePort); + + // Start the native listener. + final cListener = config.cListenerInit(receivePort.sendPort.nativePort); + if (cListener == null || cListener == nullptr) { + hasError = true; + } else { + _cListeners.add(cListener); + } + }); + + if (hasError) { + try { + throw latestNativeError( + dartMsg: 'Failed to initialize a sync native listener'); + } finally { + _stop(); + } + } + + _debugLog('started'); + } + + // stop() is called when the stream subscription is paused or canceled + void _stop() { + _debugLog('stopping'); + assert(finished, 'Stopping an unfinished group?!'); + + final cErrorCodes = _cListeners + .map(C.dartc_sync_listener_close) // map() is lazy + .toList(growable: false); // call toList() to execute immediately + _cListeners.clear(); + + _receivePorts.forEach((rp) => rp.close()); + _receivePorts.clear(); + + // throw on native, if any + cErrorCodes.forEach(checkObx); + + _debugLog('stopped'); + } + + void _debugLog(String message) { + // print('Listener ${name}: $message'); + } } /// [ObjectBox Sync](https://objectbox.io/sync/) makes data available and @@ -269,7 +581,6 @@ class Sync { if (syncClientsStorage.containsKey(store)) { throw Exception('Only one sync client can be active for a store'); } - syncOrObserversExclusive.mark(store); final client = SyncClient(store, serverUri, creds); syncClientsStorage[store] = client; InternalStoreAccess.addCloseListener(store, client, client.close); diff --git a/objectbox/lib/src/transaction.dart b/objectbox/lib/src/transaction.dart index d83eaa111..a0230dd56 100644 --- a/objectbox/lib/src/transaction.dart +++ b/objectbox/lib/src/transaction.dart @@ -43,8 +43,8 @@ class Transaction { Transaction(this._store, TxMode mode) : _isWrite = mode == TxMode.write, _cTxn = mode == TxMode.write - ? C.txn_write(_store.ptr) - : C.txn_read(_store.ptr) { + ? C.txn_write(InternalStoreAccess.ptr(_store)) + : C.txn_read(InternalStoreAccess.ptr(_store)) { checkObxPtr(_cTxn, 'failed to create transaction'); } diff --git a/objectbox/lib/src/util.dart b/objectbox/lib/src/util.dart index 8b3a059e2..f96b0c09e 100644 --- a/objectbox/lib/src/util.dart +++ b/objectbox/lib/src/util.dart @@ -5,23 +5,3 @@ import 'sync.dart'; /// Global internal storage of sync clients - one client per store. final Map syncClientsStorage = {}; - -// Currently, either SyncClient or Observers can be used at the same time. -// TODO: lift this condition after #142 is fixed. -class SyncOrObserversExclusive { - final _map = {}; - - void mark(Store store) { - if (_map.containsKey(store)) { - throw Exception( - 'Using observers/query streams in combination with SyncClient is currently not supported'); - } - _map[store] = true; - } - - void unmark(Store store) { - _map.remove(store); - } -} - -final syncOrObserversExclusive = SyncOrObserversExclusive(); diff --git a/objectbox/pubspec.yaml b/objectbox/pubspec.yaml index 80aed5ada..41d6f950d 100644 --- a/objectbox/pubspec.yaml +++ b/objectbox/pubspec.yaml @@ -5,9 +5,11 @@ homepage: https://objectbox.io description: ObjectBox is a super-fast NoSQL ACID compliant object database. environment: - # sdk: '>=2.12.0-0 <3.0.0' - # min 2.7.0 because of ffigen - sdk: '>=2.7.0 <3.0.0' + # minimum Dart SDK (also see generator/pubspec.yaml) + # v2.9.0 (Flutter v1.20) for package 'dart:ffi' NativeApi.initializeApiDLData + # v1.12.0 (Flutter v1.26) increases DART_API_DL_MAJOR_VERSION, breaking async-callbacks & observers + sdk: '>=2.9.0 <3.0.0' + # sdk: '>=2.12.0-0 <3.0.0' dependencies: collection: ^1.14.11 @@ -37,10 +39,12 @@ ffigen: headers: entry-points: - 'lib/src/bindings/objectbox.h' + - 'lib/src/bindings/objectbox-dart.h' include-directives: - - '**objectbox.h' + - '**objectbox*.h' functions: rename: + 'obx_dart_(.*)': 'dartc_$1' 'obx_(.*)': '$1' enums: member-rename: diff --git a/objectbox/test/basics_test.dart b/objectbox/test/basics_test.dart index b60f3b5c8..cc1eed103 100644 --- a/objectbox/test/basics_test.dart +++ b/objectbox/test/basics_test.dart @@ -2,8 +2,13 @@ import 'dart:ffi' as ffi; import 'package:objectbox/internal.dart'; import 'package:objectbox/src/bindings/bindings.dart'; import 'package:objectbox/src/bindings/helpers.dart'; +import 'package:objectbox/src/store.dart'; import 'package:test/test.dart'; +import 'entity.dart'; +import 'objectbox.g.dart'; +import 'test_env.dart'; + void main() { // Prior to Dart 2.6, the exception wasn't accessible and may have crashed. // Similarly, this occured in Fluter for Linux (desktop). @@ -34,4 +39,20 @@ void main() { } expect(foundLargeUid, isTrue); }); + + test('store reference', () { + final env = TestEnv('basics'); + final store1 = env.store; + final store2 = Store.fromReference(getObjectBoxModel(), store1.reference); + expect(store1, isNot(store2)); + expect(InternalStoreAccess.ptr(store1), InternalStoreAccess.ptr(store2)); + + final id = store1.box().put(TestEntity(tString: 'foo')); + expect(id, 1); + final read = store2.box().get(id); + expect(read, isNotNull); + expect(read /*!*/ .tString, 'foo'); + store2.close(); + store1.close(); + }); } diff --git a/objectbox/test/isolates_test.dart b/objectbox/test/isolates_test.dart new file mode 100644 index 000000000..c1ccbafb4 --- /dev/null +++ b/objectbox/test/isolates_test.dart @@ -0,0 +1,157 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:objectbox/objectbox.dart'; +import 'package:test/test.dart'; + +import 'entity.dart'; +import 'objectbox.g.dart'; +import 'test_env.dart'; + +// We want to have types explicit - verifying the return types of functions. +// ignore_for_file: omit_local_variable_types +void main() { + /// Set up a simple echo isolate with request-response communication. + /// This isn't really a test, just an example of how isolates can communicate. + test('isolates two-way communication example', () async { + final receivePort = ReceivePort(); + final isolate = await Isolate.spawn(echoIsolate, receivePort.sendPort); + + var sendPortCompleter = Completer(); + Completer responseCompleter; + receivePort.listen((dynamic data) { + if (data is SendPort) { + sendPortCompleter.complete(data); + } else { + print('Main received: $data'); + responseCompleter.complete(data); + } + }); + + // Receive the SendPort from the Isolate + SendPort sendPort = await sendPortCompleter.future; + + final call = (String message) { + responseCompleter = Completer(); + sendPort.send(message); + return responseCompleter.future; + }; + + // Send a message to the isolate + expect(await call('hello'), equals('re:hello')); + expect(await call('foo'), equals('re:foo')); + + isolate.kill(priority: Isolate.immediate); + receivePort.close(); + }); + + /// Work with a single store across multiple isolates. + test('single store in multiple isolates', () async { + final receivePort = ReceivePort(); + final isolate = + await Isolate.spawn(createDataIsolate, receivePort.sendPort); + + final sendPortCompleter = Completer(); + Completer responseCompleter; + receivePort.listen((dynamic data) { + if (data is SendPort) { + sendPortCompleter.complete(data); + } else { + print('Main received: $data'); + responseCompleter.complete(data); + } + }); + + // Receive the SendPort from the Isolate + SendPort sendPort = await sendPortCompleter.future; + + final call = (dynamic message) { + responseCompleter = Completer(); + sendPort.send(message); + return responseCompleter.future; + }; + + // Pass the store to the isolate + final env = TestEnv('isolates'); + expect(await call(env.store.reference), equals('store set')); + + { + // check simple box operations + expect(env.box.isEmpty(), isTrue); + expect(await call(['put', 'Foo']), equals(1)); // returns inserted id = 1 + expect(env.box.get(1).tString, equals('Foo')); + } + + { + // verify that query streams (using observers) work fine across isolates + final query = env.box.query().build(); + final futureFirst = query.findStream().first; // starts a subscription + expect(await call(['put', 'Bar']), equals(2)); + List found = await futureFirst.timeout(defaultTimeout); + expect(found.length, equals(2)); + expect(found.last.tString, equals('Bar')); + query.close(); + } + + expect(await call(['close']), equals('done')); + + isolate.kill(); + receivePort.close(); + env.close(); + }); +} + +// Echoes back any received message. +void echoIsolate(SendPort sendPort) async { + // Open the ReceivePort to listen for incoming messages + final port = ReceivePort(); + + // Send the port where the main isolate can contact us + sendPort.send(port.sendPort); + + // Listen for messages + await for (final data in port) { + // `data` is the message received. + print('Isolate received: $data'); + sendPort.send('re:$data'); + } +} + +// Creates data in the background, in the [Store] received as the first message. +void createDataIsolate(SendPort sendPort) async { + // Open the ReceivePort to listen for incoming messages + final port = ReceivePort(); + + // Send the port where the main isolate can contact us + sendPort.send(port.sendPort); + + Store store; + // Listen for messages + await for (final msg in port) { + if (store == null) { + // first message data is Store's C pointer address + store = Store.fromReference(getObjectBoxModel(), msg as ByteData); + sendPort.send('store set'); + } else { + print('Isolate received: $msg'); + if (msg is! List) { + sendPort.send('unknown message type, list expected'); + } else { + final data = msg as List; + switch (data[0]) { + case 'put': + final id = Box(store).put(TestEntity(tString: data[1])); + sendPort.send(id); + break; + case 'close': + store.close(); + sendPort.send('done'); + break; + default: + sendPort.send('unknown message: $data'); + } + } + } + } +} diff --git a/objectbox/test/observer_test.dart b/objectbox/test/observer_test.dart index 4d5eea4d0..c68795fde 100644 --- a/objectbox/test/observer_test.dart +++ b/objectbox/test/observer_test.dart @@ -1,6 +1,5 @@ -import 'dart:ffi'; +import 'dart:async'; -import 'package:objectbox/src/bindings/bindings.dart'; import 'package:test/test.dart'; import 'entity.dart'; @@ -8,75 +7,10 @@ import 'entity2.dart'; import 'objectbox.g.dart'; import 'test_env.dart'; -// ignore_for_file: non_constant_identifier_names - -/// Pointer.fromAddress(0) does not fire at all -Pointer randomPtr = Pointer.fromAddress(1337); - -int callbackSingleTypeCounter = 0; - -void callbackSingleType(Pointer user_data) { - expect(user_data.address, randomPtr.address); - callbackSingleTypeCounter++; -} - -int callbackAnyTypeCounter = 0; - -void callbackAnyType( - Pointer user_data, Pointer mutated_ids, int mutated_count) { - expect(user_data.address, randomPtr.address); - callbackAnyTypeCounter++; -} - -// dart callback signatures -typedef Single = void Function(Pointer); -typedef Any = void Function(Pointer, Pointer, int); - -class ObservableSingle { - static /*late*/ Pointer observer; - static /*late*/ Single single; - Store store; - - ObservableSingle.fromStore(this.store); - - static void _singleCallback(Pointer user_data) { - single(user_data); - } - - void observeSingleType(int entityId, Single fn, Pointer identifier) { - single = fn; - final callback = - Pointer.fromFunction(_singleCallback); - observer = C.observe_single_type(store.ptr, entityId, callback, identifier); - } -} - -class ObservableMany { - static /*late*/ Pointer observer; - static /*late*/ Any any; - Store store; - - ObservableMany.fromStore(this.store); - - static void _anyCallback( - Pointer user_data, Pointer mutated_ids, int mutated_count) { - any(user_data, mutated_ids, mutated_count); - } - - void observe(Any fn, Pointer identifier) { - any = fn; - final callback = Pointer.fromFunction(_anyCallback); - observer = C.observe(store.ptr, callback, identifier); - } -} - void main() async { /*late final*/ TestEnv env; - /*late final*/ Box box; - /*late final*/ Store store; - - final testEntityId = - getObjectBoxModel().model.findEntityByName('TestEntity').id.id; + /*late final*/ + Box box; final simpleStringItems = () => [ 'One', @@ -87,93 +21,128 @@ void main() async { 'Six' ].map((s) => TestEntity(tString: s)).toList().cast(); - final simpleNumberItems = () => [1, 2, 3, 4, 5, 6] - .map((s) => TestEntity(tInt: s)) - .toList() - .cast(); - setUp(() { env = TestEnv('observers'); box = env.box; - store = env.store; }); - /// Non static function can't be used for ffi, but you can call a dynamic function - /// aka closure inside a static function - // void callbackAnyTypeNonStatic(Pointer user_data, Pointer mutated_ids, int mutated_count) { - // expect(user_data.address, 0); - // expect(mutated_count, 1); - // } - - test('Observe any entity with class member callback', () async { - final o = ObservableMany.fromStore(store); - var putCount = 0; - o.observe((Pointer user_data, Pointer mutated_ids, - int mutated_count) { - expect(user_data.address, randomPtr.address); - putCount++; - }, randomPtr); - - box.putMany(simpleStringItems()); - simpleStringItems().forEach((i) => box.put(i)); - simpleNumberItems().forEach((i) => box.put(i)); - - C.observer_close(ObservableMany.observer); - expect(putCount, 13); + tearDown(() { + env.close(); }); - test('Observe a single entity with class member callback', () async { - final o = ObservableSingle.fromStore(store); - var putCount = 0; - o.observeSingleType(testEntityId, (Pointer user_data) { - putCount++; - }, randomPtr); - + test('Observe single entity', () async { + Completer completer; + var expectedEvents = 0; + + final stream = env.store.subscribe(); + final subscription = stream.listen((_) { + print('TestEntity updated'); + expectedEvents--; + if (expectedEvents == 0) { + completer.complete(); + } + }); + + // expect two events after one put() and one putMany() + expectedEvents = 2; + completer = Completer(); + box.put(simpleStringItems().first); + Box(env.store).put(TestEntity2()); box.putMany(simpleStringItems()); - simpleStringItems().forEach((i) => box.put(i)); - simpleNumberItems().forEach((i) => box.put(i)); - - C.observer_close(ObservableSingle.observer); - expect(putCount, 13); + await completer.future.timeout(defaultTimeout); + expect(expectedEvents, 0); + + // cancel the subscription + await subscription.cancel(); + + // make sure there are no more events after cancelling + expectedEvents = 1; + completer = Completer(); + box.put(simpleStringItems().first); + expect(completer.future.timeout(defaultTimeout), + throwsA(isA())); + expect(expectedEvents, 1); // note: unchanged, no events received anymore }); - test('Observe any entity with static callback', () async { - final callback = Pointer.fromFunction(callbackAnyType); - final observer = C.observe(store.ptr, callback, Pointer.fromAddress(1337)); + test('Observe multiple entities', () async { + Completer completer; + var expectedEvents = 0; + var typesUpdates = {}; // number of events per entity type - box.putMany(simpleStringItems()); + final stream = env.store.subscribeAll(); - box.remove(1); + final subscription = stream.listen((entityType) { + print('Entity updated: $entityType'); + expectedEvents--; - // update value - final entity2 = box.get(2); - entity2.tString = 'Dva'; - box.put(entity2); + if (typesUpdates[entityType] == null) { + typesUpdates[entityType] = 0; + } + typesUpdates[entityType]++; - final box2 = Box(store); - box2.put(TestEntity2()); - box2.remove(1); - box2.put(TestEntity2()); - - expect(callbackAnyTypeCounter, 6); - C.observer_close(observer); - }); - - test('Observe single entity', () async { - final callback = - Pointer.fromFunction(callbackSingleType); - final observer = - C.observe_single_type(store.ptr, testEntityId, callback, randomPtr); + if (expectedEvents == 0) { + completer.complete(); + } + }); + // expect three events: two puts() (separate entities), one putMany() + expectedEvents = 3; + completer = Completer(); + box.put(simpleStringItems().first); + Box(env.store).put(TestEntity2()); box.putMany(simpleStringItems()); - simpleStringItems().forEach((i) => box.put(i)); - simpleNumberItems().forEach((i) => box.put(i)); - - expect(callbackSingleTypeCounter, 13); - C.observer_close(observer); + await completer.future.timeout(defaultTimeout); + expect(expectedEvents, 0); + expect(typesUpdates.keys, sameAsList([TestEntity, TestEntity2])); + expect(typesUpdates[TestEntity], 2); + expect(typesUpdates[TestEntity2], 1); + + // cancel the subscription + await subscription.cancel(); + + // make sure there are no more events after cancelling + expectedEvents = 1; + completer = Completer(); + box.put(simpleStringItems().first); + expect(completer.future.timeout(defaultTimeout), + throwsA(isA())); + expect(expectedEvents, 1); // note: unchanged, no events received anymore }); - tearDown(() { - env.close(); + test('Observer pause/resume', () async { + final testPauseResume = (Stream stream) async { + Completer completer; + final subscription = stream.listen((dynamic _) { + completer.complete(); + }); + + // triggers when listening + completer = Completer(); + box.put(simpleStringItems().first); + await completer.future.timeout(defaultTimeout); + + // won't trigger when paused + subscription.pause(); + completer = Completer(); + box.put(simpleStringItems().first); + expect(completer.future.timeout(defaultTimeout), + throwsA(isA())); + + // triggers when resumed (Note: no buffering of previous events) + subscription.resume(); + completer = Completer(); + box.put(simpleStringItems().first); + await completer.future.timeout(defaultTimeout); + + // won't trigger when cancelled + await subscription.cancel(); + completer = Completer(); + box.put(simpleStringItems().first); + expect(completer.future.timeout(defaultTimeout), + throwsA(isA())); + }; + + await testPauseResume(env.store.subscribe()); + await testPauseResume(env.store.subscribeAll()); }); } diff --git a/objectbox/test/query_test.dart b/objectbox/test/query_test.dart index 6c549b90b..752d88abe 100644 --- a/objectbox/test/query_test.dart +++ b/objectbox/test/query_test.dart @@ -9,7 +9,8 @@ import 'test_env.dart'; void main() { /*late final*/ TestEnv env; - /*late final*/ Box box; + /*late final*/ + Box box; setUp(() { env = TestEnv('query'); @@ -240,8 +241,8 @@ void main() { final q3 = box.query(text.equals("can't find this")).build(); final result3 = q3.findIds(); - expect(result0, unorderedEqualsInts([2, 3, 4, 5, 6, 7])); - expect(result2, unorderedEqualsInts([7])); + expect(result0, sameAsList([2, 3, 4, 5, 6, 7])); + expect(result2, sameAsList([7])); expect(result3.isEmpty, isTrue); q0.close(); diff --git a/objectbox/test/relations_test.dart b/objectbox/test/relations_test.dart index d33d1fd33..4cb241c5b 100644 --- a/objectbox/test/relations_test.dart +++ b/objectbox/test/relations_test.dart @@ -324,22 +324,19 @@ void main() { expect(b[2].tString, 'not referenced'); final strings = (TestEntity e) => e.tString; - expect(b[0].testEntities.map(strings), unorderedEqualsStrings(['foo'])); - expect(b[1].testEntities.map(strings), - unorderedEqualsStrings(['bar', 'bar2'])); + expect(b[0].testEntities.map(strings), sameAsList(['foo'])); + expect(b[1].testEntities.map(strings), sameAsList(['bar', 'bar2'])); expect(b[2].testEntities.length, isZero); // Update an existing target. b[1].testEntities.add(env.box.get(1)); // foo - expect(b[1].testEntities.map(strings), - unorderedEqualsStrings(['foo', 'bar', 'bar2'])); + expect( + b[1].testEntities.map(strings), sameAsList(['foo', 'bar', 'bar2'])); b[1].testEntities.removeWhere((e) => e.tString == 'bar'); - expect(b[1].testEntities.map(strings), - unorderedEqualsStrings(['foo', 'bar2'])); + expect(b[1].testEntities.map(strings), sameAsList(['foo', 'bar2'])); boxB.put(b[1]); b[1] = boxB.get(b[1].id); - expect(b[1].testEntities.map(strings), - unorderedEqualsStrings(['foo', 'bar2'])); + expect(b[1].testEntities.map(strings), sameAsList(['foo', 'bar2'])); // Insert a new target, already with some "source" entities pointing to it. var newB = RelatedEntityB(); @@ -353,11 +350,11 @@ void main() { expect(env.box.get(4).tString, equals('newly created from B')); newB = boxB.get(newB.id); expect(newB.testEntities.map(strings), - unorderedEqualsStrings(['foo', 'newly created from B'])); + sameAsList(['foo', 'newly created from B'])); // The previous put also affects b[1], 'foo' is not related anymore. b[1] = boxB.get(b[1].id); - expect(b[1].testEntities.map(strings), unorderedEqualsStrings(['bar2'])); + expect(b[1].testEntities.map(strings), sameAsList(['bar2'])); }); test('query', () { @@ -394,22 +391,19 @@ void main() { expect(a[2].tInt, 3); final strings = (TestEntity e) => e.tString; - expect(a[0].testEntities.map(strings), unorderedEqualsStrings(['foo'])); - expect(a[1].testEntities.map(strings), - unorderedEqualsStrings(['bar', 'bar2'])); + expect(a[0].testEntities.map(strings), sameAsList(['foo'])); + expect(a[1].testEntities.map(strings), sameAsList(['bar', 'bar2'])); expect(a[2].testEntities.length, isZero); // Update an existing target. a[1].testEntities.add(env.box.get(1)); // foo - expect(a[1].testEntities.map(strings), - unorderedEqualsStrings(['foo', 'bar', 'bar2'])); + expect( + a[1].testEntities.map(strings), sameAsList(['foo', 'bar', 'bar2'])); a[1].testEntities.removeWhere((e) => e.tString == 'bar'); - expect(a[1].testEntities.map(strings), - unorderedEqualsStrings(['foo', 'bar2'])); + expect(a[1].testEntities.map(strings), sameAsList(['foo', 'bar2'])); boxA.put(a[1]); a[1] = boxA.get(a[1].id); - expect(a[1].testEntities.map(strings), - unorderedEqualsStrings(['foo', 'bar2'])); + expect(a[1].testEntities.map(strings), sameAsList(['foo', 'bar2'])); // Insert a new target with some "source" entities pointing to it. var newA = RelatedEntityA(tInt: 4); @@ -423,11 +417,10 @@ void main() { expect(env.box.get(4).tString, equals('newly created from A')); newA = boxA.get(newA.id); expect(newA.testEntities.map(strings), - unorderedEqualsStrings(['foo', 'newly created from A'])); + sameAsList(['foo', 'newly created from A'])); // The previous put also affects TestEntity(foo) - added target (tInt=4). - expect( - env.box.get(1).relManyA.map(toInt), unorderedEqualsInts([1, 2, 4])); + expect(env.box.get(1).relManyA.map(toInt), sameAsList([1, 2, 4])); }); test('query', () { diff --git a/objectbox/test/stream_test.dart b/objectbox/test/stream_test.dart index cdf112f97..920061050 100644 --- a/objectbox/test/stream_test.dart +++ b/objectbox/test/stream_test.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:test/test.dart'; import 'entity.dart'; @@ -32,16 +30,14 @@ void main() { box.put(TestEntity(tString: 'Hello world')); - // The delay is here to ensure that the callback execution is executed - // sequentially, otherwise the testing framework's execution will be - // prioritized (for some reason), before any callback. - await Future.delayed(Duration(seconds: 0)); + await yieldExecution(); box.putMany([ TestEntity(tString: 'Goodbye'), TestEntity(tString: 'for now') ]); - await Future.delayed(Duration(seconds: 0)); + + await yieldExecution(); expect(result, ['Hello world', 'for now, Goodbye, Hello world']); @@ -60,14 +56,16 @@ void main() { }); box.put(TestEntity(tString: 'Hello world')); - await Future.delayed(Duration(seconds: 0)); + + await yieldExecution(); // idem, see above box.putMany([ TestEntity(tString: 'Goodbye'), TestEntity(tString: 'for now') ]); - await Future.delayed(Duration(seconds: 0)); + + await yieldExecution(); expect(result, [1, 3]); @@ -99,7 +97,7 @@ void main() { final t2 = TestEntity2(); box2.put(t2); - await Future.delayed(Duration(seconds: 0)); + await yieldExecution(); expect(counter1, 0); expect(counter2, 1); @@ -107,7 +105,7 @@ void main() { final t1 = TestEntity(); box.put(t1); - await Future.delayed(Duration(seconds: 0)); + await yieldExecution(); expect(counter1, 1); expect(counter2, 1); @@ -115,7 +113,7 @@ void main() { final ts1 = [1, 2, 3].map((i) => TestEntity(tInt: i)).toList(); box.putMany(ts1); - await Future.delayed(Duration(seconds: 0)); + await yieldExecution(); expect(counter1, 2); expect(counter2, 1); @@ -123,7 +121,7 @@ void main() { final ts2 = [1, 2, 3].map((i) => TestEntity2()).toList(); box2.putMany(ts2); - await Future.delayed(Duration(seconds: 0)); + await yieldExecution(); expect(counter1, 2); expect(counter2, 2); diff --git a/objectbox/test/sync_test.dart b/objectbox/test/sync_test.dart index f3fadf227..5af1b988d 100644 --- a/objectbox/test/sync_test.dart +++ b/objectbox/test/sync_test.dart @@ -1,12 +1,13 @@ +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:objectbox/src/bindings/bindings.dart'; -import 'package:objectbox/objectbox.dart'; import 'package:objectbox/internal.dart'; import 'package:test/test.dart'; import 'entity.dart'; +import 'entity2.dart'; import 'objectbox.g.dart'; import 'test_env.dart'; @@ -17,19 +18,23 @@ void main() { /*late final*/ TestEnv env; /*late final*/ Store store; + TestEnv /*?*/ env2; + int serverPort = 9999; setUp(() { env = TestEnv('sync'); store = env.store; + env2 = TestEnv('sync2'); }); tearDown(() { - if (env != null) env.close(); + env?.close(); + env2?.close(); }); // lambda to easily create clients in the test below SyncClient createClient(Store s) => - Sync.client(s, 'ws://127.0.0.1:9999', SyncCredentials.none()); + Sync.client(s, 'ws://127.0.0.1:$serverPort', SyncCredentials.none()); // lambda to easily create clients in the test below SyncClient loggedInClient(Store s) { @@ -59,24 +64,6 @@ void main() { if (Sync.isAvailable()) { // TESTS to run when SYNC is available - group('Circumvent issue #142 - async callbacks error', () { - final error = throwsA(predicate((Exception e) => e.toString().contains( - 'Using observers/query streams in combination with SyncClient is currently not supported'))); - - test('Must not start an Observer when SyncClient is active', () { - createClient(store); - expect(() => env.box.query().build().findStream(), error); - }); - - test('Must not start SyncClient when an Observer is active', () { - final error = throwsA(predicate((Exception e) => e.toString().contains( - 'Using observers/query streams in combination with SyncClient is currently not supported'))); - - createClient(store); - expect(() => env.box.query().build().findStream(), error); - }); - }); - test('SyncClient lifecycle', () { expect(store.syncClient(), isNull); @@ -116,7 +103,6 @@ void main() { }); test('SyncClient is closed when a store is closed', () { - final env2 = TestEnv('sync2'); final client = createClient(env2.store); env2.close(); expect(client.isClosed(), isTrue); @@ -125,7 +111,6 @@ void main() { test('different Store => different SyncClient', () { SyncClient c1 = createClient(store); - final env2 = TestEnv('sync2'); SyncClient c2 = createClient(env2.store); expect(c1, isNot(equals(c2))); env2.close(); @@ -178,29 +163,188 @@ void main() { expect(c.state(), equals(SyncState.stopped)); }); - test('SyncClient - data test (requires manual server setup)', () { - final env2 = TestEnv('sync2'); + group('Sync tests with server', () { + SyncServer server; + setUp(() async { + server = SyncServer(); + await server.start(); + serverPort = server.port; + }); + + tearDown(() async { + print('Waiting for the server to stop'); + await server.stop(); + print('Server has stopped'); + }); + + test('SyncClient data sync', () async { + await server.online(); + final client1 = loggedInClient(env.store); + final client2 = loggedInClient(env2.store); + + int id = env.box.put(TestEntity(tLong: Random().nextInt(1 << 32))); + expect(waitUntil(() => env2.box.get(id) != null), isTrue); + + TestEntity /*?*/ read1 = env.box.get(id); + TestEntity /*?*/ read2 = env2.box.get(id); + expect(read1, isNotNull); + expect(read2, isNotNull); + expect(read1 /*!*/ .id, equals(read2 /*!*/ .id)); + expect(read1 /*!*/ .tLong, equals(read2 /*!*/ .tLong)); + client1.close(); + client2.close(); + }); + + test('SyncClient listeners: connection', () async { + final client = createClient(env.store); + + // collect connection events + final events = []; + final streamSub = client.connectionEvents.listen(events.add); + + // multiple subscriptions work as well + final events2 = []; + final streamSub2 = client.connectionEvents.listen(events2.add); + + await server.online(); + client.start(); + + expect(waitUntil(() => client.state() == SyncState.loggedIn), isTrue); + await yieldExecution(); + expect(events, equals([SyncConnectionEvent.connected])); + expect(events2, equals([SyncConnectionEvent.connected])); + + await streamSub2.cancel(); + + await server.stop(keepDb: true); + + expect( + waitUntil(() => client.state() == SyncState.disconnected), isTrue); + await yieldExecution(); + expect( + events, + equals([ + SyncConnectionEvent.connected, + SyncConnectionEvent.disconnected + ])); + + await server.start(keepDb: true); + await server.online(); + + expect(waitUntil(() => client.state() == SyncState.loggedIn), isTrue); + await yieldExecution(); + + expect( + events, + equals([ + SyncConnectionEvent.connected, + SyncConnectionEvent.disconnected, + SyncConnectionEvent.connected + ])); + expect(events2, equals([SyncConnectionEvent.connected])); + + await streamSub.cancel(); + client.close(); + }); + + test('SyncClient listeners: login', () async { + final client = createClient(env.store); - loggedInClient(env.store); - loggedInClient(env2.store); + client.setCredentials(SyncCredentials.sharedSecretString('foo')); - int id = env.box.put(TestEntity(tLong: Random().nextInt(1 << 32))); - expect(waitUntil(() => env2.box.get(id) != null), isTrue); + // collect login events + final events = []; + client.loginEvents.listen(events.add); - TestEntity /*?*/ read1 = env.box.get(id); - TestEntity /*?*/ read2 = env2.box.get(id); - expect(read1, isNotNull); - expect(read2, isNotNull); - expect(read1 /*!*/ .id, equals(read2 /*!*/ .id)); - expect(read1 /*!*/ .tLong, equals(read2 /*!*/ .tLong)); + await server.online(); + client.start(); + + expect(await client.loginEvents.first.timeout(defaultTimeout), + equals(SyncLoginEvent.credentialsRejected)); + + client.setCredentials(SyncCredentials.none()); + + expect(waitUntil(() => client.state() == SyncState.loggedIn), isTrue); + await yieldExecution(); + expect( + events, + equals( + [SyncLoginEvent.credentialsRejected, SyncLoginEvent.loggedIn])); + + client.close(); + }); + + test('SyncClient listeners: completion', () async { + await server.online(); + final client = loggedInClient(store); + expect(env.box.isEmpty(), isTrue); + int id = env.box.put(TestEntity(tLong: 100)); + + // Note: wait for the client to finish sending to the server. + // There's currently no other way to recognize this. + sleep(Duration(milliseconds: 100)); + client.close(); + + final client2 = loggedInClient(env2.store); + await client2.completionEvents.first.timeout(defaultTimeout); + client2.close(); + + expect(env2.box.get(id) /*!*/ .tLong, 100); + }); + + test('SyncClient listeners: changes', () async { + await server.online(); + final client = loggedInClient(store); + final client2 = loggedInClient(env2.store); + + final events = >[]; + client2.changeEvents.listen(events.add); + + expect(env2.box.get(1), isNull); + + env.box.put(TestEntity(tString: 'foo')); + env.store.runInTransaction(TxMode.write, () { + Box(env.store).put(TestEntity2()); // not synced + env.box.put(TestEntity(tString: 'bar')); + env.box.put(TestEntity(tString: 'oof')); + env.box.remove(1); + }); + + // wait for the data to be transferred + expect(waitUntil(() => env2.box.count() == 2), isTrue); + + // check the events + await yieldExecution(); + expect(events.length, 2); + + // env.box.put(TestEntity(tString: 'foo')); + expect(events[0].length, 1); + expect(events[0][0].entity, TestEntity); + expect(events[0][0].entityId, 1); + expect(events[0][0].puts, [1]); + expect(events[0][0].removals, isEmpty); + + // env.store.runInTransaction(TxMode.Write, () { + // Box(env.store).put(TestEntity2()); // not synced + // env.box.put(TestEntity(tString: 'bar')); + // env.box.put(TestEntity(tString: 'oof')); + // env.box.remove(1); + // }); + expect(events[1].length, 1); + expect(events[1][0].entity, TestEntity); + expect(events[1][0].entityId, 1); + expect(events[1][0].puts, [2, 3]); + expect(events[1][0].removals, [1]); + + client.close(); + client2.close(); + }); }, - // Note: only available when you start a sync server manually. - // Comment out the `skip: ` argument in tthe test-case definition. - // run sync-server --unsecured-no-authentication --model=/path/objectbox-dart/test/objectbox-model.json - skip: 'Data sync test is disabled, Enable after running sync-server.' // - ); + skip: SyncServer.isAvailable() + ? null + : 'sync-server executable is not available in PATH - tests requiring it are skipped'); } else { - // TESTS to run when SYNC isn't available + // TESTS to run when SYNC is NOT available test('SyncClient cannot be created when running with non-sync library', () { expect( @@ -210,3 +354,86 @@ void main() { }); } } + +/// sync-server process wrapper for testing clients +class SyncServer { + Directory /*?*/ dir; + int /*?*/ port; + Future /*?*/ process; + + static bool isAvailable() { + // Note: this causes an additional valgrind summary output with a leak. + // Unfortunately, it seems like we can't do anything about that... + // Tried running with Process.start() but that didn't help. There currently + // doesn't seem to be a way to check if a command is available so we have to + // live with that. + // At least, the additional error report doesn't cause valgrind to fail. + try { + Process.runSync('sync-server', ['--help']); + return true; + } on ProcessException { + //print(e); + return false; + } + } + + void start({bool keepDb = false}) async { + port ??= await _getUnusedPort(); + + dir ??= Directory('testdata-sync-server-$port'); + if (!keepDb) _deleteDb(); + + process = Process.start('sync-server', [ + '--unsecured-no-authentication', + '--db-directory=${dir.path}', + '--model=${Directory.current.path}/test/objectbox-model.json', + '--bind=ws://127.0.0.1:$port', + '--browser-bind=http://127.0.0.1:${await _getUnusedPort()}' + ]); + } + + /// Wait for the server to respond to a simple http request. + /// This simple check speeds up test by only trying to log in after the server + /// has started, avoiding the reconnect backoff intervals altogether. + Future online() async => Future(() async { + final httpClient = HttpClient(); + while (true) { + try { + await httpClient.get('127.0.0.1', port, ''); + break; + } on SocketException catch (e) { + // only retry if "connection refused" + if (e.osError.errorCode != 111) rethrow; + await Future.delayed(Duration(milliseconds: 1)); + } + } + httpClient.close(force: true); + }).timeout(defaultTimeout); + + void stop({bool keepDb = false}) async { + if (process == null) return; + final proc = await process /*!*/; + process = null; + proc.kill(ProcessSignal.sigint); + final exitCode = await proc.exitCode; + if (exitCode != 0) { + await stdout.addStream(proc.stdout); + await stderr.addStream(proc.stderr); + expect(await proc.exitCode, isZero); + } + if (!keepDb) _deleteDb(); + } + + Future _getUnusedPort() => + ServerSocket.bind(InternetAddress.loopbackIPv4, 0).then((socket) { + var port = socket.port; + socket.close(); + return port; + }); + + void _deleteDb() { + if (dir != null && dir /*!*/ .existsSync()) { + dir /*!*/ .deleteSync(recursive: true); + } + } +} diff --git a/objectbox/test/test_env.dart b/objectbox/test/test_env.dart index 1c249779c..346ad5a37 100644 --- a/objectbox/test/test_env.dart +++ b/objectbox/test/test_env.dart @@ -21,22 +21,30 @@ class TestEnv { void close() { store.close(); - if (dir.existsSync()) dir.deleteSync(recursive: true); + if (dir != null && dir /*!*/ .existsSync()) { + dir /*!*/ .deleteSync(recursive: true); + } } } +const defaultTimeout = Duration(milliseconds: 1000); + /// "Busy-waits" until the predicate returns true. -bool waitUntil(bool Function() predicate, {int timeoutMs = 1000}) { +bool waitUntil(bool Function() predicate, {Duration timeout = defaultTimeout}) { var success = false; - final until = DateTime.now().millisecondsSinceEpoch + timeoutMs; + final until = DateTime.now().add(timeout); - while (!(success = predicate()) && - until > DateTime.now().millisecondsSinceEpoch) { + while (!(success = predicate()) && until.isAfter(DateTime.now())) { sleep(Duration(milliseconds: 1)); } return success; } -Matcher unorderedEqualsStrings(List list) => unorderedEquals(list); +/// same as package:test unorderedEquals() but statically typed +Matcher sameAsList(List list) => unorderedEquals(list); -Matcher unorderedEqualsInts(List list) => unorderedEquals(list); +// Yield execution to other isolates. +// +// We need to do this to receive an event in the stream before processing +// the remainder of the test case. +final yieldExecution = () async => await Future.delayed(Duration.zero); diff --git a/objectbox/tool/valgrind.sh b/objectbox/tool/valgrind.sh index 52716f590..016a24a05 100755 --- a/objectbox/tool/valgrind.sh +++ b/objectbox/tool/valgrind.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash set -euo pipefail +# Note: when you see the process seemingly stuck after printing "All tests passed!", it's because dart2native produced +# binaries will wait for any background isolates to finish before stopping the process. +# For example, the following code has the issue: `HttpClient().get(...)` +# while `final httpClient = HttpClient(); httpClient.get(...); httpClient.close(force: true);` works fine. + root=$( cd "$(dirname "$0")/.." pwd -P @@ -17,20 +22,18 @@ fi testDir="${root}/build/test/valgrind" rm -rf "${testDir}" mkdir -pv "${testDir}" -cd "${testDir}" || exit 1 - function testWithValgrind() { echo "Running $1 with valgrind" - - dart2native "${root}/test/${1}" --output ./test --verbose + testExe="${testDir}/${1%.*}" + dart2native "${root}/test/${1}" --output "${testExe}" --verbose valgrind \ --leak-check=full \ --error-exitcode=1 \ --show-mismatched-frees=no \ --show-possibly-lost=no \ --errors-for-leak-kinds=definite \ - ./test + "${testExe}" echo "$1 successful - no errors reported by valgrind" echo "--------------------------------------------------------------------------------" @@ -39,12 +42,10 @@ function testWithValgrind() { if [[ "$#" -gt "0" ]]; then testWithValgrind "${1}_test.dart" else - for file in "${root}/test/"*_test.dart - do - testWithValgrind $(basename $file) + for file in "${root}/test/"*_test.dart; do + testWithValgrind "$(basename "$file")" done fi echo "Test passed, cleaning up" -cd "${root}" || exit 1 -rm -rf "${testDir}" \ No newline at end of file +rm -rf "${testDir}" diff --git a/sync_flutter_libs/android/build.gradle b/sync_flutter_libs/android/build.gradle index e3c96498d..66f976390 100644 --- a/sync_flutter_libs/android/build.gradle +++ b/sync_flutter_libs/android/build.gradle @@ -12,5 +12,5 @@ android { dependencies { // https://bintray.com/objectbox/objectbox/io.objectbox%3Aobjectbox-android - implementation "io.objectbox:objectbox-android:2.8.0-sync" + implementation "io.objectbox:objectbox-android:2.9.0-sync" } diff --git a/sync_flutter_libs/ios/download-framework.sh b/sync_flutter_libs/ios/download-framework.sh index 21b97fc39..28109a8d4 100755 --- a/sync_flutter_libs/ios/download-framework.sh +++ b/sync_flutter_libs/ios/download-framework.sh @@ -3,23 +3,23 @@ set -euo pipefail # NOTE: run this script before publishing -echo "Sync-enabled objectbox-swift isn't released yet" -exit 1 - # https://github.com/objectbox/objectbox-swift/releases/ -obxSwiftVersion="1.4.1" +obxSwiftVersion="1.5.0-sync-rc5" dir=$(dirname "$0") -url="https://github.com/objectbox/objectbox-swift/releases/download/v${obxSwiftVersion}/ObjectBox-framework-${obxSwiftVersion}.zip" +#url="https://github.com/objectbox/objectbox-swift/releases/download/v${obxSwiftVersion}/ObjectBox-framework-${obxSwiftVersion}.zip" +url="https://github.com/objectbox/objectbox-swift-spec-staging/releases/download/v1.x/ObjectBox-xcframework-${obxSwiftVersion}.zip" zip="${dir}/fw.zip" curl --location --fail --output "${zip}" "${url}" +frameworkPath=Carthage/Build/ObjectBox.xcframework/ios-arm64/ObjectBox.framework + rm -rf "${dir}/Carthage" unzip "${zip}" -d "${dir}" \ - "Carthage/Build/iOS/ObjectBox.framework/Headers/*" \ - "Carthage/Build/iOS/ObjectBox.framework/ObjectBox" \ - "Carthage/Build/iOS/ObjectBox.framework/Info.plist" + "${frameworkPath}/Headers/*" \ + "${frameworkPath}/ObjectBox" \ + "${frameworkPath}/Info.plist" rm "${zip}" \ No newline at end of file diff --git a/sync_flutter_libs/pubspec.yaml b/sync_flutter_libs/pubspec.yaml index aca55c125..3e201dad3 100644 --- a/sync_flutter_libs/pubspec.yaml +++ b/sync_flutter_libs/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://objectbox.io description: ObjectBox is a super-fast NoSQL ACID compliant object database. This package contains flutter runtime libraries for ObjectBox, including ObjectBox Sync. environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.0 <2.0.0" + sdk: ">=2.9.0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" dependencies: # This is here just to ensure compatibility between objectbox-dart code and the libraries used diff --git a/tool/publish.sh b/tool/publish.sh index 4b0bc4d74..34c0b9146 100755 --- a/tool/publish.sh +++ b/tool/publish.sh @@ -4,8 +4,7 @@ echo "Downloading iOS dependencies for package flutter libs" "${root}"/flutter_libs/ios/download-framework.sh -# TODO enable once objectbox-swift with Sync is released -#"${root}"/sync_flutter_libs/ios/download-framework.sh +"${root}"/sync_flutter_libs/ios/download-framework.sh echo "Commenting-out Carthage in .gitignore in flutter libs" update flutter_libs/ios/.gitignore "s/^Carthage/#Carthage/g"