diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a2fe6ce61..eb991bed0 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,7 +20,7 @@ jobs:
- name: Check scores
shell: bash
run: |
- if [[ "90" -gt "${{ steps.analysis.outputs.total }}" ]]; then
- echo "Score is less then 90, please check the analysis report and resolve the issues"
+ if [[ "100" -gt "${{ steps.analysis.outputs.total }}" ]]; then
+ echo "Score is less then 100, please check the analysis report and resolve the issues"
exit 1
fi
\ No newline at end of file
diff --git a/generator/integration-tests/basics/1.dart b/generator/integration-tests/basics/1.dart
index 6e5f23d74..34ffc3272 100644
--- a/generator/integration-tests/basics/1.dart
+++ b/generator/integration-tests/basics/1.dart
@@ -4,7 +4,6 @@ import 'lib/objectbox.g.dart';
import 'package:test/test.dart';
import '../test_env.dart';
import '../common.dart';
-import 'package:objectbox/src/bindings/bindings.dart';
void main() {
TestEnv env;
diff --git a/generator/integration-tests/indexes/1.dart b/generator/integration-tests/indexes/1.dart
index fe84b5a31..4047a5dbb 100644
--- a/generator/integration-tests/indexes/1.dart
+++ b/generator/integration-tests/indexes/1.dart
@@ -6,7 +6,6 @@ import 'lib/objectbox.g.dart';
import 'package:test/test.dart';
import '../test_env.dart';
import '../common.dart';
-import 'package:objectbox/src/bindings/bindings.dart';
void main() {
TestEnv env;
diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart
index 2279880df..9ce1f601d 100644
--- a/generator/lib/src/code_chunks.dart
+++ b/generator/lib/src/code_chunks.dart
@@ -1,6 +1,6 @@
import 'dart:convert';
+
import 'package:objectbox/src/modelinfo/index.dart';
-import 'package:objectbox/src/bindings/bindings.dart';
import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError;
class CodeChunks {
diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart
index 1366cc773..938940064 100644
--- a/generator/lib/src/entity_resolver.dart
+++ b/generator/lib/src/entity_resolver.dart
@@ -7,8 +7,6 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:objectbox/objectbox.dart';
import 'package:objectbox/internal.dart';
-import 'package:objectbox/src/bindings/bindings.dart';
-import 'package:objectbox/src/bindings/helpers.dart';
import 'package:objectbox/src/modelinfo/index.dart';
import 'package:source_gen/source_gen.dart';
diff --git a/objectbox/analysis_options.yaml b/objectbox/analysis_options.yaml
index 9a1bc5b36..a11934aa9 100644
--- a/objectbox/analysis_options.yaml
+++ b/objectbox/analysis_options.yaml
@@ -27,7 +27,8 @@ analyzer:
- example/**
- generator/**
- benchmark/**
- - lib/src/bindings/objectbox-c.dart
+ - lib/src/modelinfo/enums.dart
+ - lib/src/native/bindings/objectbox-c.dart
- lib/flatbuffers/** # Not really our code
- test/objectbox.g.dart # TODO remove exception after #168 is implemented
strong-mode:
diff --git a/objectbox/lib/integration_test.dart b/objectbox/lib/integration_test.dart
index f54ced1fd..6361f8fbc 100644
--- a/objectbox/lib/integration_test.dart
+++ b/objectbox/lib/integration_test.dart
@@ -1,7 +1,7 @@
library integration_test;
-import './src/bindings/bindings.dart';
-import './src/bindings/helpers.dart';
+import './src/native/bindings/bindings.dart' as native;
+import './src/native/bindings/helpers.dart' as native;
import 'internal.dart';
// ignore_for_file: public_member_api_docs
@@ -31,6 +31,6 @@ class IntegrationTest {
modelInfo.validate();
final model = Model(modelInfo);
- checkObx(C.model_free(model.ptr));
+ native.checkObx(native.C.model_free(model.ptr));
}
}
diff --git a/objectbox/lib/internal.dart b/objectbox/lib/internal.dart
index 6542143aa..8fc0d8318 100644
--- a/objectbox/lib/internal.dart
+++ b/objectbox/lib/internal.dart
@@ -4,7 +4,7 @@ library objectbox_internal;
export 'src/model.dart';
export 'src/modelinfo/index.dart';
-export 'src/query/query.dart'
+export 'src/query.dart'
// don't export the same things as objectbox.dart to avoid docs conflicts
hide
Query,
diff --git a/objectbox/lib/objectbox.dart b/objectbox/lib/objectbox.dart
index 75aeee343..8801bcb61 100644
--- a/objectbox/lib/objectbox.dart
+++ b/objectbox/lib/objectbox.dart
@@ -9,7 +9,7 @@ export 'src/annotations.dart';
export 'src/box.dart' show Box;
export 'src/common.dart';
export 'src/observable.dart';
-export 'src/query/query.dart'
+export 'src/query.dart'
show
Query,
QueryBuilder,
diff --git a/objectbox/lib/src/box.dart b/objectbox/lib/src/box.dart
index 49072063e..05ac67c16 100644
--- a/objectbox/lib/src/box.dart
+++ b/objectbox/lib/src/box.dart
@@ -1,325 +1 @@
-import 'dart:ffi';
-
-import 'package:ffi/ffi.dart' show allocate, free;
-
-import 'bindings/bindings.dart';
-import 'bindings/flatbuffers.dart';
-import 'bindings/helpers.dart';
-import 'bindings/structs.dart';
-import 'modelinfo/index.dart';
-import 'query/query.dart';
-import 'relations/info.dart';
-import 'relations/to_many.dart';
-import 'relations/to_one.dart';
-import 'store.dart';
-import 'transaction.dart';
-
-/// Box put (write) mode.
-enum PutMode {
- /// Insert (if given object's ID is zero) or update an existing object.
- put,
-
- /// Insert a new object.
- insert,
-
- /// Update an existing object, fails if the given ID doesn't exist.
- update,
-}
-
-/// A Box instance gives you access to objects of a particular type.
-/// You get Box instances via [Store.box()] or [Box(Store)].
-///
-/// For example, if you have User and Order entities, you need two Box objects
-/// to interact with each:
-/// ```dart
-/// Box userBox = store.box();
-/// Box orderBox = store.box();
-/// ```
-class Box {
- final Store _store;
- final Pointer _cBox;
- final EntityDefinition _entity;
- final bool _hasToOneRelations;
- final bool _hasToManyRelations;
- final _builder = BuilderWithCBuffer();
-
- /// Create a box for an Entity.
- factory Box(Store store) => store.box();
-
- Box._(this._store, this._entity)
- : _hasToOneRelations = _entity.model.properties
- .any((ModelProperty prop) => prop.isRelation),
- _hasToManyRelations = _entity.model.relations.isNotEmpty ||
- _entity.model.backlinks.isNotEmpty,
- _cBox = C.box(_store.ptr, _entity.model.id.id) {
- checkObxPtr(_cBox, 'failed to create box');
- }
-
- bool get _hasRelations => _hasToOneRelations || _hasToManyRelations;
-
- static int _getOBXPutMode(PutMode mode) {
- switch (mode) {
- case PutMode.put:
- return OBXPutMode.PUT;
- case PutMode.insert:
- return OBXPutMode.INSERT;
- case PutMode.update:
- return OBXPutMode.UPDATE;
- }
- throw Exception('Invalid put mode ' + mode.toString());
- }
-
- /// Puts the given Object in the box (aka persisting it).
- ///
- /// If this is a new object (its ID property is 0), a new ID will be assigned
- /// to the object (and returned).
- ///
- /// If the object with given was already in the box, it will be overwritten.
- ///
- /// Performance note: consider [putMany] to put several objects at once.
- int put(T object, {PutMode mode = PutMode.put}) {
- if (_hasRelations) {
- final tx = Transaction(_store, TxMode.write);
- try {
- final id = _put(object, mode, tx);
- tx.markSuccessful();
- return id;
- } finally {
- tx.close();
- }
- } else {
- return _put(object, mode, null);
- }
- }
-
- int _put(T object, PutMode mode, Transaction /*?*/ tx) {
- if (_hasRelations) {
- if (tx == null) {
- throw Exception(
- 'Invalid state: can only use _put() on an entity with relations when executing from inside a write transaction.');
- }
- if (_hasToOneRelations) _putToOneRelFields(object, mode, tx);
- }
- var id = _entity.objectToFB(object, _builder.fbb);
- final newId = C.box_put_object4(_cBox, _builder.bufPtr.cast(),
- _builder.fbb.size, _getOBXPutMode(mode));
- id = _handlePutObjectResult(object, id, newId);
- if (_hasToManyRelations) _putToManyRelFields(object, mode, tx);
- _builder.resetIfLarge();
- return id;
- }
-
- /// Puts the given [objects] into this Box in a single transaction.
- ///
- /// Returns a list of all IDs of the inserted Objects.
- List putMany(List objects, {PutMode mode = PutMode.put}) {
- if (objects.isEmpty) return [];
-
- final putIds = List.filled(objects.length, 0);
-
- final tx = Transaction(_store, TxMode.write);
- try {
- if (_hasToOneRelations) {
- objects.forEach((object) => _putToOneRelFields(object, mode, tx));
- }
-
- final cursor = tx.cursor(_entity);
- final cMode = _getOBXPutMode(mode);
- for (var i = 0; i < objects.length; i++) {
- final object = objects[i];
- _builder.fbb.reset();
- final id = _entity.objectToFB(object, _builder.fbb);
- final newId = C.cursor_put_object4(
- cursor.ptr, _builder.bufPtr.cast(), _builder.fbb.size, cMode);
- putIds[i] = _handlePutObjectResult(object, id, newId);
- }
-
- if (_hasToManyRelations) {
- objects.forEach((object) => _putToManyRelFields(object, mode, tx));
- }
- _builder.resetIfLarge();
- tx.markSuccessful();
- } finally {
- tx.close();
- }
-
- return putIds;
- }
-
- // Checks if native obx_*_put_object() was successful (result is a valid ID).
- // Sets the given ID on the object if previous ID was zero (new object).
- int _handlePutObjectResult(T object, int prevId, int result) {
- if (result == 0) throw latestNativeError(dartMsg: 'object put failed');
- if (prevId == 0) _entity.setId(object, result);
- return result;
- }
-
- /// Retrieves the stored object with the ID [id] from this box's database.
- /// Returns null if an object with the given ID doesn't exist.
- T /*?*/ get(int id) {
- final tx = Transaction(_store, TxMode.read);
- try {
- return tx.cursor(_entity).get(id);
- } finally {
- tx.close();
- }
- }
-
- /// Returns a list of [ids.length] Objects of type T, each corresponding to
- /// the location of its ID in [ids]. Non-existent IDs become null.
- ///
- /// Pass growableResult: true for the resulting list to be growable.
- List getMany(List ids, {bool growableResult = false}) {
- final result = List.filled(ids.length, null, growable: growableResult);
- if (ids.isEmpty) return result;
- final tx = Transaction(_store, TxMode.read);
- try {
- final cursor = tx.cursor(_entity);
- for (var i = 0; i < ids.length; i++) {
- final object = cursor.get(ids[i]);
- if (object != null) result[i] = object;
- }
- return result;
- } finally {
- tx.close();
- }
- }
-
- /// Returns all stored objects in this Box.
- List getAll() {
- final tx = Transaction(_store, TxMode.read);
- try {
- final cursor = tx.cursor(_entity);
- final result = [];
- var code = C.cursor_first(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr);
- while (code != OBX_NOT_FOUND) {
- checkObx(code);
- result.add(_entity.objectFromFB(_store, cursor.readData));
- code = C.cursor_next(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr);
- }
- return result;
- } finally {
- tx.close();
- }
- }
-
- /// Returns a builder to create queries for Object matching supplied criteria.
- QueryBuilder query([Condition /*?*/ qc]) =>
- QueryBuilder(_store, _entity, qc);
-
- /// Returns the count of all stored Objects in this box.
- /// If [limit] is not zero, stops counting at the given limit.
- int count({int limit = 0}) {
- final count = allocate();
- try {
- checkObx(C.box_count(_cBox, limit, count));
- return count.value;
- } finally {
- free(count);
- }
- }
-
- /// Returns true if no objects are in this box.
- bool isEmpty() {
- final isEmpty = allocate();
- try {
- checkObx(C.box_is_empty(_cBox, isEmpty));
- return isEmpty.value == 1;
- } finally {
- free(isEmpty);
- }
- }
-
- /// Returns true if this box contains an Object with the ID [id].
- bool contains(int id) {
- final contains = allocate();
- try {
- checkObx(C.box_contains(_cBox, id, contains));
- return contains.value == 1;
- } finally {
- free(contains);
- }
- }
-
- /// Returns true if this box contains objects with all of the given [ids].
- bool containsMany(List ids) {
- final contains = allocate();
- try {
- return executeWithIdArray(ids, (ptr) {
- checkObx(C.box_contains_many(_cBox, ptr, contains));
- return contains.value == 1;
- });
- } finally {
- free(contains);
- }
- }
-
- /// Removes (deletes) the Object with the given [id]. Returns true if the
- /// object was present (and thus removed), otherwise returns false.
- bool remove(int id) {
- final err = C.box_remove(_cBox, id);
- if (err == OBX_NOT_FOUND) return false;
- checkObx(err); // throws on other errors
- return true;
- }
-
- /// Removes (deletes) by ID, returning a list of IDs of all removed Objects.
- int removeMany(List ids) {
- final countRemoved = allocate();
- try {
- return executeWithIdArray(ids, (ptr) {
- checkObx(C.box_remove_many(_cBox, ptr, countRemoved));
- return countRemoved.value;
- });
- } finally {
- free(countRemoved);
- }
- }
-
- /// Removes (deletes) ALL Objects in a single transaction.
- int removeAll() {
- final removedItems = allocate();
- try {
- checkObx(C.box_remove_all(_cBox, removedItems));
- return removedItems.value;
- } finally {
- free(removedItems);
- }
- }
-
- /// The low-level pointer to this box.
- Pointer get ptr => _cBox;
-
- void _putToOneRelFields(T object, PutMode mode, Transaction tx) {
- _entity.toOneRelations(object).forEach((ToOne rel) {
- if (!rel.hasValue) return;
- rel.attach(_store);
- // put new objects
- if (rel.targetId == 0) {
- rel.targetId =
- InternalToOneAccess.targetBox(rel)._put(rel.target, mode, tx);
- }
- });
- }
-
- void _putToManyRelFields(T object, PutMode mode, Transaction tx) {
- _entity.toManyRelations(object).forEach((RelInfo info, ToMany rel) {
- if (InternalToManyAccess.hasPendingDbChanges(rel)) {
- InternalToManyAccess.setRelInfo(rel, _store, info, this);
- rel.applyToDb(mode: mode, tx: tx);
- }
- });
- }
-}
-
-/// Internal only.
-// TODO enable annotation once meta:1.3.0 is out
-// @internal
-class InternalBoxAccess {
- /// Create a box in the store for the given entity.
- static Box create(Store store, EntityDefinition entity) =>
- Box._(store, entity);
-
- /// Close the box, freeing resources.
- static void close(Box box) => box._builder.clear();
-}
+export 'native/box.dart' if (dart.library.html) 'web/box.dart';
diff --git a/objectbox/lib/src/common.dart b/objectbox/lib/src/common.dart
index 29d5825dd..fd3674034 100644
--- a/objectbox/lib/src/common.dart
+++ b/objectbox/lib/src/common.dart
@@ -1,9 +1,3 @@
-import 'dart:ffi';
-
-import 'package:ffi/ffi.dart' show allocate, free;
-
-import 'bindings/bindings.dart';
-
// TODO use pub_semver?
/// Wrapper for a semantic version information.
class Version {
@@ -23,22 +17,6 @@ class Version {
String toString() => '$major.$minor.$patch';
}
-/// Returns the underlying ObjectBox-C library version.
-Version nativeLibraryVersion() {
- var majorPtr = allocate(),
- minorPtr = allocate(),
- patchPtr = allocate();
-
- try {
- C.version(majorPtr, minorPtr, patchPtr);
- return Version(majorPtr.value, minorPtr.value, patchPtr.value);
- } finally {
- free(majorPtr);
- free(minorPtr);
- free(patchPtr);
- }
-}
-
/// ObjectBox native exception wrapper.
class ObjectBoxException implements Exception {
/// Dart message related to this native error.
diff --git a/objectbox/lib/src/model.dart b/objectbox/lib/src/model.dart
index 1f73ef95d..41d940590 100644
--- a/objectbox/lib/src/model.dart
+++ b/objectbox/lib/src/model.dart
@@ -1,112 +1 @@
-import 'dart:ffi';
-
-import 'package:ffi/ffi.dart';
-
-import 'bindings/bindings.dart';
-import 'bindings/helpers.dart';
-import 'common.dart';
-import 'modelinfo/index.dart';
-
-// ignore_for_file: public_member_api_docs
-
-class Model {
- final Pointer _cModel;
-
- Pointer get ptr => _cModel;
-
- Model(ModelInfo model)
- : _cModel = checkObxPtr(C.model(), 'failed to create model') {
- try {
- model.entities.forEach(addEntity);
-
- // set last entity id
- C.model_last_entity_id(
- _cModel, model.lastEntityId.id, model.lastEntityId.uid);
-
- // set last relation id
- if (model.lastRelationId != null) {
- C.model_last_relation_id(
- _cModel, model.lastRelationId.id, model.lastRelationId.uid);
- }
-
- // set last index id
- if (model.lastIndexId != null) {
- C.model_last_index_id(
- _cModel, model.lastIndexId.id, model.lastIndexId.uid);
- }
- } catch (e) {
- C.model_free(_cModel);
- rethrow;
- }
- }
-
- void _check(int errorCode) {
- if (errorCode == OBX_SUCCESS) return;
-
- throw ObjectBoxException(
- dartMsg: 'Model building failed',
- nativeCode: C.model_error_code(_cModel),
- nativeMsg: cString(C.model_error_message(_cModel)));
- }
-
- void addEntity(ModelEntity entity) {
- // start entity
- var name = Utf8.toUtf8(entity.name).cast();
- try {
- _check(C.model_entity(_cModel, name, entity.id.id, entity.id.uid));
- } finally {
- free(name);
- }
-
- if (entity.flags != 0) {
- // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_model_entity_flags() exists.
- try {
- _check(C.model_entity_flags(_cModel, entity.flags));
- } on ArgumentError {
- // flags not supported; don't do anything until objectbox-c v0.11
- // this should only be used from our test code
- }
- }
-
- // add all properties
- entity.properties.forEach(addProperty);
-
- // set last property id
- _check(C.model_entity_last_property_id(
- _cModel, entity.lastPropertyId.id, entity.lastPropertyId.uid));
-
- entity.relations.forEach(addRelation);
- }
-
- void addProperty(ModelProperty prop) {
- var name = Utf8.toUtf8(prop.name).cast();
- try {
- _check(
- C.model_property(_cModel, name, prop.type, prop.id.id, prop.id.uid));
-
- if (prop.isRelation) {
- var relTarget = Utf8.toUtf8(prop.relationTarget /*!*/).cast();
- try {
- _check(C.model_property_relation(_cModel, relTarget,
- prop.indexId /*!*/ .id, prop.indexId /*!*/ .uid));
- } finally {
- free(relTarget);
- }
- } else if (prop.indexId != null) {
- _check(C.model_property_index_id(
- _cModel, prop.indexId.id, prop.indexId.uid));
- }
- } finally {
- free(name);
- }
-
- if (prop.flags != 0) {
- _check(C.model_property_flags(_cModel, prop.flags));
- }
- }
-
- void addRelation(ModelRelation rel) {
- _check(C.model_relation(
- _cModel, rel.id.id, rel.id.uid, rel.targetId.id, rel.targetId.uid));
- }
-}
+export 'native/model.dart' if (dart.library.html) 'web/model.dart';
diff --git a/objectbox/lib/src/modelinfo/enums.dart b/objectbox/lib/src/modelinfo/enums.dart
new file mode 100644
index 000000000..97510e0fa
--- /dev/null
+++ b/objectbox/lib/src/modelinfo/enums.dart
@@ -0,0 +1,160 @@
+// Note: the enums in this file are copied from native/bindings/objectbox-c.dart
+// to avoid package:ffi import which would break compatibility with web.
+
+import '../annotations.dart';
+
+/// Maps [OBXPropertyType] to its string representation (name).
+String obxPropertyTypeToString(int type) {
+ switch (type) {
+ case OBXPropertyType.Bool:
+ return 'bool';
+ case OBXPropertyType.Byte:
+ return 'byte';
+ case OBXPropertyType.Short:
+ return 'short';
+ case OBXPropertyType.Char:
+ return 'char';
+ case OBXPropertyType.Int:
+ return 'int';
+ case OBXPropertyType.Long:
+ return 'long';
+ case OBXPropertyType.Float:
+ return 'float';
+ case OBXPropertyType.Double:
+ return 'double';
+ case OBXPropertyType.String:
+ return 'string';
+ case OBXPropertyType.Date:
+ return 'date';
+ case OBXPropertyType.Relation:
+ return 'relation';
+ case OBXPropertyType.DateNano:
+ return 'dateNano';
+ case OBXPropertyType.ByteVector:
+ return 'byteVector';
+ case OBXPropertyType.StringVector:
+ return 'stringVector';
+ }
+
+ throw Exception('Invalid OBXPropertyType: $type');
+}
+
+int propertyTypeToOBXPropertyType(PropertyType type) {
+ switch (type) {
+ case PropertyType.byte:
+ return OBXPropertyType.Byte;
+ case PropertyType.short:
+ return OBXPropertyType.Short;
+ case PropertyType.char:
+ return OBXPropertyType.Char;
+ case PropertyType.int:
+ return OBXPropertyType.Int;
+ case PropertyType.float:
+ return OBXPropertyType.Float;
+ case PropertyType.date:
+ return OBXPropertyType.Date;
+ case PropertyType.dateNano:
+ return OBXPropertyType.DateNano;
+ case PropertyType.byteVector:
+ return OBXPropertyType.ByteVector;
+ }
+ throw Exception('Invalid PropertyType: $type');
+}
+
+/// /// Bit-flags defining the behavior of entities.
+/// /// Note: Numbers indicate the bit position
+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;
+}
+
+/// /// Bit-flags defining the behavior of properties.
+/// /// Note: Numbers indicate the bit position
+abstract class OBXPropertyFlags {
+ /// /// 64 bit long property (internally unsigned) representing the ID of the entity.
+ /// /// May be combined with: NON_PRIMITIVE_TYPE, ID_MONOTONIC_SEQUENCE, ID_SELF_ASSIGNABLE.
+ static const int ID = 1;
+
+ /// /// On languages like Java, a non-primitive type is used (aka wrapper types, allowing null)
+ static const int NON_PRIMITIVE_TYPE = 2;
+
+ /// /// Unused yet
+ static const int NOT_NULL = 4;
+ static const int INDEXED = 8;
+
+ /// /// Unused yet
+ static const int RESERVED = 16;
+
+ /// /// Unique index
+ static const int UNIQUE = 32;
+
+ /// /// Unused yet: Use a persisted sequence to enforce ID to rise monotonic (no ID reuse)
+ static const int ID_MONOTONIC_SEQUENCE = 64;
+
+ /// /// Allow IDs to be assigned by the developer
+ static const int ID_SELF_ASSIGNABLE = 128;
+
+ /// /// Unused yet
+ static const int INDEX_PARTIAL_SKIP_NULL = 256;
+
+ /// /// Used by References for 1) back-references and 2) to clear references to deleted objects (required for ID reuse)
+ static const int INDEX_PARTIAL_SKIP_ZERO = 512;
+
+ /// /// Virtual properties may not have a dedicated field in their entity class, e.g. target IDs of to-one relations
+ static const int VIRTUAL = 1024;
+
+ /// /// Index uses a 32 bit hash instead of the value
+ /// /// 32 bits is shorter on disk, runs well on 32 bit systems, and should be OK even with a few collisions
+ static const int INDEX_HASH = 2048;
+
+ /// /// Index uses a 64 bit hash instead of the value
+ /// /// recommended mostly for 64 bit machines with values longer >200 bytes; small values are faster with a 32 bit hash
+ static const int INDEX_HASH64 = 4096;
+
+ /// /// The actual type of the variable is unsigned (used in combination with numeric OBXPropertyType_*).
+ /// /// While our default are signed ints, queries & indexes need do know signing info.
+ /// /// Note: Don't combine with ID (IDs are always unsigned internally).
+ static const int UNSIGNED = 8192;
+
+ /// /// By defining an ID companion property, a special ID encoding scheme is activated involving this property.
+ /// ///
+ /// /// For Time Series IDs, a companion property of type Date or DateNano represents the exact timestamp.
+ static const int ID_COMPANION = 16384;
+}
+
+abstract class OBXPropertyType {
+ /// ///< 1 byte
+ static const int Bool = 1;
+
+ /// ///< 1 byte
+ static const int Byte = 2;
+
+ /// ///< 2 bytes
+ static const int Short = 3;
+
+ /// ///< 1 byte
+ static const int Char = 4;
+
+ /// ///< 4 bytes
+ static const int Int = 5;
+
+ /// ///< 8 bytes
+ static const int Long = 6;
+
+ /// ///< 4 bytes
+ static const int Float = 7;
+
+ /// ///< 8 bytes
+ static const int Double = 8;
+ static const int String = 9;
+
+ /// ///< Unix timestamp (milliseconds since 1970) in 8 bytes
+ static const int Date = 10;
+ static const int Relation = 11;
+
+ /// ///< Unix timestamp (nanoseconds since 1970) in 8 bytes
+ static const int DateNano = 12;
+ static const int ByteVector = 23;
+ static const int StringVector = 30;
+}
diff --git a/objectbox/lib/src/modelinfo/index.dart b/objectbox/lib/src/modelinfo/index.dart
index a7a5897d3..8a18272b8 100644
--- a/objectbox/lib/src/modelinfo/index.dart
+++ b/objectbox/lib/src/modelinfo/index.dart
@@ -1,4 +1,5 @@
export 'entity_definition.dart';
+export 'enums.dart';
export 'iduid.dart';
export 'model_definition.dart';
export 'modelbacklink.dart';
diff --git a/objectbox/lib/src/modelinfo/modelentity.dart b/objectbox/lib/src/modelinfo/modelentity.dart
index 7c6ff8da1..f935ff1f5 100644
--- a/objectbox/lib/src/modelinfo/modelentity.dart
+++ b/objectbox/lib/src/modelinfo/modelentity.dart
@@ -1,4 +1,4 @@
-import '../bindings/bindings.dart';
+import 'enums.dart';
import 'iduid.dart';
import 'modelbacklink.dart';
import 'modelinfo.dart';
diff --git a/objectbox/lib/src/modelinfo/modelproperty.dart b/objectbox/lib/src/modelinfo/modelproperty.dart
index 7cb52085a..7f4905774 100644
--- a/objectbox/lib/src/modelinfo/modelproperty.dart
+++ b/objectbox/lib/src/modelinfo/modelproperty.dart
@@ -1,5 +1,4 @@
-import '../bindings/bindings.dart';
-import '../bindings/helpers.dart';
+import 'enums.dart';
import 'iduid.dart';
import 'modelentity.dart';
diff --git a/objectbox/lib/src/bindings/bindings.dart b/objectbox/lib/src/native/bindings/bindings.dart
similarity index 100%
rename from objectbox/lib/src/bindings/bindings.dart
rename to objectbox/lib/src/native/bindings/bindings.dart
diff --git a/objectbox/lib/src/bindings/data_visitor.dart b/objectbox/lib/src/native/bindings/data_visitor.dart
similarity index 98%
rename from objectbox/lib/src/bindings/data_visitor.dart
rename to objectbox/lib/src/native/bindings/data_visitor.dart
index 79f39c4da..3866c9f47 100644
--- a/objectbox/lib/src/bindings/data_visitor.dart
+++ b/objectbox/lib/src/native/bindings/data_visitor.dart
@@ -2,7 +2,7 @@ import 'dart:ffi';
import 'package:ffi/ffi.dart' show allocate, free;
-import '../modelinfo/entity_definition.dart';
+import '../../modelinfo/entity_definition.dart';
import '../store.dart';
import 'bindings.dart';
diff --git a/objectbox/lib/src/bindings/flatbuffers.dart b/objectbox/lib/src/native/bindings/flatbuffers.dart
similarity index 98%
rename from objectbox/lib/src/bindings/flatbuffers.dart
rename to objectbox/lib/src/native/bindings/flatbuffers.dart
index 658202aa3..3110abd11 100644
--- a/objectbox/lib/src/bindings/flatbuffers.dart
+++ b/objectbox/lib/src/native/bindings/flatbuffers.dart
@@ -4,7 +4,7 @@ import 'dart:typed_data';
import 'package:ffi/ffi.dart' as f;
-import '../../flatbuffers/flat_buffers.dart' as fb;
+import '../../../flatbuffers/flat_buffers.dart' as fb;
// ignore_for_file: public_member_api_docs
diff --git a/objectbox/lib/src/bindings/helpers.dart b/objectbox/lib/src/native/bindings/helpers.dart
similarity index 58%
rename from objectbox/lib/src/bindings/helpers.dart
rename to objectbox/lib/src/native/bindings/helpers.dart
index e89ec2740..06cda99c9 100644
--- a/objectbox/lib/src/bindings/helpers.dart
+++ b/objectbox/lib/src/native/bindings/helpers.dart
@@ -3,9 +3,8 @@ import 'dart:typed_data';
import 'package:ffi/ffi.dart';
-import '../annotations.dart';
-import '../common.dart';
-import '../modelinfo/entity_definition.dart';
+import '../../common.dart';
+import '../../modelinfo/entity_definition.dart';
import '../store.dart';
import 'bindings.dart';
@@ -56,63 +55,6 @@ String cString(Pointer charPtr) {
return Utf8.fromUtf8(charPtr.cast());
}
-String obxPropertyTypeToString(int type) {
- switch (type) {
- case OBXPropertyType.Bool:
- return 'bool';
- case OBXPropertyType.Byte:
- return 'byte';
- case OBXPropertyType.Short:
- return 'short';
- case OBXPropertyType.Char:
- return 'char';
- case OBXPropertyType.Int:
- return 'int';
- case OBXPropertyType.Long:
- return 'long';
- case OBXPropertyType.Float:
- return 'float';
- case OBXPropertyType.Double:
- return 'double';
- case OBXPropertyType.String:
- return 'string';
- case OBXPropertyType.Date:
- return 'date';
- case OBXPropertyType.Relation:
- return 'relation';
- case OBXPropertyType.DateNano:
- return 'dateNano';
- case OBXPropertyType.ByteVector:
- return 'byteVector';
- case OBXPropertyType.StringVector:
- return 'stringVector';
- }
-
- throw Exception('Invalid OBXPropertyType: $type');
-}
-
-int propertyTypeToOBXPropertyType(PropertyType type) {
- switch (type) {
- case PropertyType.byte:
- return OBXPropertyType.Byte;
- case PropertyType.short:
- return OBXPropertyType.Short;
- case PropertyType.char:
- return OBXPropertyType.Char;
- case PropertyType.int:
- return OBXPropertyType.Int;
- case PropertyType.float:
- return OBXPropertyType.Float;
- case PropertyType.date:
- return OBXPropertyType.Date;
- case PropertyType.dateNano:
- return OBXPropertyType.DateNano;
- case PropertyType.byteVector:
- return OBXPropertyType.ByteVector;
- }
- throw Exception('Invalid PropertyType: $type');
-}
-
class CursorHelper {
final EntityDefinition _entity;
final Store _store;
diff --git a/objectbox/lib/src/bindings/objectbox-c.dart b/objectbox/lib/src/native/bindings/objectbox-c.dart
similarity index 100%
rename from objectbox/lib/src/bindings/objectbox-c.dart
rename to objectbox/lib/src/native/bindings/objectbox-c.dart
diff --git a/objectbox/lib/src/bindings/objectbox.h b/objectbox/lib/src/native/bindings/objectbox.h
similarity index 100%
rename from objectbox/lib/src/bindings/objectbox.h
rename to objectbox/lib/src/native/bindings/objectbox.h
diff --git a/objectbox/lib/src/bindings/structs.dart b/objectbox/lib/src/native/bindings/structs.dart
similarity index 99%
rename from objectbox/lib/src/bindings/structs.dart
rename to objectbox/lib/src/native/bindings/structs.dart
index 653d63d6c..db18428b4 100644
--- a/objectbox/lib/src/bindings/structs.dart
+++ b/objectbox/lib/src/native/bindings/structs.dart
@@ -3,7 +3,7 @@ import 'dart:typed_data' show Uint8List;
import 'package:ffi/ffi.dart' show allocate, free, Utf8;
-import '../common.dart';
+import '../../common.dart';
import 'bindings.dart';
// ignore_for_file: public_member_api_docs
diff --git a/objectbox/lib/src/native/box.dart b/objectbox/lib/src/native/box.dart
new file mode 100644
index 000000000..4d9f8a3b8
--- /dev/null
+++ b/objectbox/lib/src/native/box.dart
@@ -0,0 +1,394 @@
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart' show allocate, free;
+
+import '../modelinfo/index.dart';
+import '../relations/info.dart';
+import '../relations/to_many.dart';
+import '../relations/to_one.dart';
+import '../store.dart';
+import '../transaction.dart';
+import 'bindings/bindings.dart';
+import 'bindings/flatbuffers.dart';
+import 'bindings/helpers.dart';
+import 'bindings/structs.dart';
+import 'query/query.dart';
+import 'transaction.dart';
+
+/// Box put (write) mode.
+enum PutMode {
+ /// Insert (if given object's ID is zero) or update an existing object.
+ put,
+
+ /// Insert a new object.
+ insert,
+
+ /// Update an existing object, fails if the given ID doesn't exist.
+ update,
+}
+
+/// A Box instance gives you access to objects of a particular type.
+/// You get Box instances via [Store.box()] or [Box(Store)].
+///
+/// For example, if you have User and Order entities, you need two Box objects
+/// to interact with each:
+/// ```dart
+/// Box userBox = store.box();
+/// Box orderBox = store.box();
+/// ```
+class Box {
+ final Store _store;
+ final Pointer _cBox;
+ final EntityDefinition _entity;
+ final bool _hasToOneRelations;
+ final bool _hasToManyRelations;
+ final _builder = BuilderWithCBuffer();
+
+ /// Create a box for an Entity.
+ factory Box(Store store) => store.box();
+
+ Box._(this._store, this._entity)
+ : _hasToOneRelations = _entity.model.properties
+ .any((ModelProperty prop) => prop.isRelation),
+ _hasToManyRelations = _entity.model.relations.isNotEmpty ||
+ _entity.model.backlinks.isNotEmpty,
+ _cBox = C.box(_store.ptr, _entity.model.id.id) {
+ checkObxPtr(_cBox, 'failed to create box');
+ }
+
+ bool get _hasRelations => _hasToOneRelations || _hasToManyRelations;
+
+ static int _getOBXPutMode(PutMode mode) {
+ switch (mode) {
+ case PutMode.put:
+ return OBXPutMode.PUT;
+ case PutMode.insert:
+ return OBXPutMode.INSERT;
+ case PutMode.update:
+ return OBXPutMode.UPDATE;
+ }
+ throw Exception('Invalid put mode ' + mode.toString());
+ }
+
+ /// Puts the given Object in the box (aka persisting it).
+ ///
+ /// If this is a new object (its ID property is 0), a new ID will be assigned
+ /// to the object (and returned).
+ ///
+ /// If the object with given was already in the box, it will be overwritten.
+ ///
+ /// Performance note: consider [putMany] to put several objects at once.
+ int put(T object, {PutMode mode = PutMode.put}) {
+ if (_hasRelations) {
+ final tx = Transaction(_store, TxMode.write);
+ try {
+ final id = _put(object, mode, tx);
+ tx.markSuccessful();
+ return id;
+ } finally {
+ tx.close();
+ }
+ } else {
+ return _put(object, mode, null);
+ }
+ }
+
+ int _put(T object, PutMode mode, Transaction /*?*/ tx) {
+ if (_hasRelations) {
+ if (tx == null) {
+ throw Exception(
+ 'Invalid state: can only use _put() on an entity with relations when executing from inside a write transaction.');
+ }
+ if (_hasToOneRelations) _putToOneRelFields(object, mode, tx);
+ }
+ var id = _entity.objectToFB(object, _builder.fbb);
+ final newId = C.box_put_object4(_cBox, _builder.bufPtr.cast(),
+ _builder.fbb.size, _getOBXPutMode(mode));
+ id = _handlePutObjectResult(object, id, newId);
+ if (_hasToManyRelations) _putToManyRelFields(object, mode, tx);
+ _builder.resetIfLarge();
+ return id;
+ }
+
+ /// Puts the given [objects] into this Box in a single transaction.
+ ///
+ /// Returns a list of all IDs of the inserted Objects.
+ List putMany(List objects, {PutMode mode = PutMode.put}) {
+ if (objects.isEmpty) return [];
+
+ final putIds = List.filled(objects.length, 0);
+
+ final tx = Transaction(_store, TxMode.write);
+ try {
+ if (_hasToOneRelations) {
+ objects.forEach((object) => _putToOneRelFields(object, mode, tx));
+ }
+
+ final cursor = tx.cursor(_entity);
+ final cMode = _getOBXPutMode(mode);
+ for (var i = 0; i < objects.length; i++) {
+ final object = objects[i];
+ _builder.fbb.reset();
+ final id = _entity.objectToFB(object, _builder.fbb);
+ final newId = C.cursor_put_object4(
+ cursor.ptr, _builder.bufPtr.cast(), _builder.fbb.size, cMode);
+ putIds[i] = _handlePutObjectResult(object, id, newId);
+ }
+
+ if (_hasToManyRelations) {
+ objects.forEach((object) => _putToManyRelFields(object, mode, tx));
+ }
+ _builder.resetIfLarge();
+ tx.markSuccessful();
+ } finally {
+ tx.close();
+ }
+
+ return putIds;
+ }
+
+ // Checks if native obx_*_put_object() was successful (result is a valid ID).
+ // Sets the given ID on the object if previous ID was zero (new object).
+ int _handlePutObjectResult(T object, int prevId, int result) {
+ if (result == 0) throw latestNativeError(dartMsg: 'object put failed');
+ if (prevId == 0) _entity.setId(object, result);
+ return result;
+ }
+
+ /// Retrieves the stored object with the ID [id] from this box's database.
+ /// Returns null if an object with the given ID doesn't exist.
+ T /*?*/ get(int id) {
+ final tx = Transaction(_store, TxMode.read);
+ try {
+ return tx.cursor(_entity).get(id);
+ } finally {
+ tx.close();
+ }
+ }
+
+ /// Returns a list of [ids.length] Objects of type T, each corresponding to
+ /// the location of its ID in [ids]. Non-existent IDs become null.
+ ///
+ /// Pass growableResult: true for the resulting list to be growable.
+ List getMany(List ids, {bool growableResult = false}) {
+ final result = List.filled(ids.length, null, growable: growableResult);
+ if (ids.isEmpty) return result;
+ final tx = Transaction(_store, TxMode.read);
+ try {
+ final cursor = tx.cursor(_entity);
+ for (var i = 0; i < ids.length; i++) {
+ final object = cursor.get(ids[i]);
+ if (object != null) result[i] = object;
+ }
+ return result;
+ } finally {
+ tx.close();
+ }
+ }
+
+ /// Returns all stored objects in this Box.
+ List getAll() {
+ final tx = Transaction(_store, TxMode.read);
+ try {
+ final cursor = tx.cursor(_entity);
+ final result = [];
+ var code = C.cursor_first(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr);
+ while (code != OBX_NOT_FOUND) {
+ checkObx(code);
+ result.add(_entity.objectFromFB(_store, cursor.readData));
+ code = C.cursor_next(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr);
+ }
+ return result;
+ } finally {
+ tx.close();
+ }
+ }
+
+ /// Returns a builder to create queries for Object matching supplied criteria.
+ QueryBuilder query([Condition /*?*/ qc]) =>
+ QueryBuilder(_store, _entity, qc);
+
+ /// Returns the count of all stored Objects in this box.
+ /// If [limit] is not zero, stops counting at the given limit.
+ int count({int limit = 0}) {
+ final count = allocate();
+ try {
+ checkObx(C.box_count(_cBox, limit, count));
+ return count.value;
+ } finally {
+ free(count);
+ }
+ }
+
+ /// Returns true if no objects are in this box.
+ bool isEmpty() {
+ final isEmpty = allocate();
+ try {
+ checkObx(C.box_is_empty(_cBox, isEmpty));
+ return isEmpty.value == 1;
+ } finally {
+ free(isEmpty);
+ }
+ }
+
+ /// Returns true if this box contains an Object with the ID [id].
+ bool contains(int id) {
+ final contains = allocate();
+ try {
+ checkObx(C.box_contains(_cBox, id, contains));
+ return contains.value == 1;
+ } finally {
+ free(contains);
+ }
+ }
+
+ /// Returns true if this box contains objects with all of the given [ids].
+ bool containsMany(List ids) {
+ final contains = allocate();
+ try {
+ return executeWithIdArray(ids, (ptr) {
+ checkObx(C.box_contains_many(_cBox, ptr, contains));
+ return contains.value == 1;
+ });
+ } finally {
+ free(contains);
+ }
+ }
+
+ /// Removes (deletes) the Object with the given [id]. Returns true if the
+ /// object was present (and thus removed), otherwise returns false.
+ bool remove(int id) {
+ final err = C.box_remove(_cBox, id);
+ if (err == OBX_NOT_FOUND) return false;
+ checkObx(err); // throws on other errors
+ return true;
+ }
+
+ /// Removes (deletes) by ID, returning a list of IDs of all removed Objects.
+ int removeMany(List ids) {
+ final countRemoved = allocate();
+ try {
+ return executeWithIdArray(ids, (ptr) {
+ checkObx(C.box_remove_many(_cBox, ptr, countRemoved));
+ return countRemoved.value;
+ });
+ } finally {
+ free(countRemoved);
+ }
+ }
+
+ /// Removes (deletes) ALL Objects in a single transaction.
+ int removeAll() {
+ final removedItems = allocate();
+ try {
+ checkObx(C.box_remove_all(_cBox, removedItems));
+ return removedItems.value;
+ } finally {
+ free(removedItems);
+ }
+ }
+
+ /// The low-level pointer to this box.
+ Pointer get ptr => _cBox;
+
+ void _putToOneRelFields(T object, PutMode mode, Transaction tx) {
+ _entity.toOneRelations(object).forEach((ToOne rel) {
+ if (!rel.hasValue) return;
+ rel.attach(_store);
+ // put new objects
+ if (rel.targetId == 0) {
+ rel.targetId =
+ InternalToOneAccess.targetBox(rel)._put(rel.target, mode, tx);
+ }
+ });
+ }
+
+ void _putToManyRelFields(T object, PutMode mode, Transaction tx) {
+ _entity.toManyRelations(object).forEach((RelInfo info, ToMany rel) {
+ if (InternalToManyAccess.hasPendingDbChanges(rel)) {
+ InternalToManyAccess.setRelInfo(rel, _store, info, this);
+ rel.applyToDb(mode: mode, tx: tx);
+ }
+ });
+ }
+}
+
+/// Internal only.
+// TODO enable annotation once meta:1.3.0 is out
+// @internal
+class InternalBoxAccess {
+ /// Create a box in the store for the given entity.
+ static Box create(Store store, EntityDefinition entity) =>
+ Box._(store, entity);
+
+ /// Close the box, freeing resources.
+ static void close(Box box) => box._builder.clear();
+
+ /// Put the object in a given transaction.
+ static int put(
+ Box box, EntityT object, PutMode mode, Transaction tx) =>
+ box._put(object, mode, tx);
+
+ /// Put a standalone relation.
+ static void relPut(
+ Box box,
+ int relationId,
+ int sourceId,
+ int targetId,
+ ) =>
+ checkObx(C.box_rel_put(box.ptr, relationId, sourceId, targetId));
+
+ /// Remove a standalone relation entry between two objects.
+ static void relRemove(
+ Box box,
+ int relationId,
+ int sourceId,
+ int targetId,
+ ) =>
+ checkObx(C.box_rel_remove(box.ptr, relationId, sourceId, targetId));
+
+ /// Read all objects in this Box related to the given object.
+ /// Similar to box.getMany() but loads the OBX_id_array and reads objects
+ /// in a single Transaction, ensuring consistency. And it's a little more
+ /// efficient for not unpacking the id array to a dart list.
+ static List getRelated(Box box, RelInfo rel) {
+ final tx = Transaction(box._store, TxMode.read);
+ try {
+ Pointer cIdsPtr;
+ switch (rel.type) {
+ case RelType.toMany:
+ cIdsPtr = C.box_rel_get_ids(box.ptr, rel.id, rel.objectId);
+ break;
+ case RelType.toOneBacklink:
+ cIdsPtr = C.box_get_backlink_ids(box.ptr, rel.id, rel.objectId);
+ break;
+ case RelType.toManyBacklink:
+ cIdsPtr = C.box_rel_get_backlink_ids(box.ptr, rel.id, rel.objectId);
+ break;
+ default:
+ throw UnimplementedError();
+ }
+ checkObxPtr(cIdsPtr);
+ final result = [];
+ try {
+ final cIds = cIdsPtr.ref;
+ if (cIds.count > 0) {
+ final cursor = tx.cursor(box._entity);
+ for (var i = 0; i < cIds.count; i++) {
+ final code = C.cursor_get(
+ cursor.ptr, cIds.ids[i], cursor.dataPtrPtr, cursor.sizePtr);
+ if (code != OBX_NOT_FOUND) {
+ checkObx(code);
+ result.add(box._entity.objectFromFB(box._store, cursor.readData));
+ }
+ }
+ }
+ } finally {
+ C.id_array_free(cIdsPtr);
+ }
+ return result;
+ } finally {
+ tx.close();
+ }
+ }
+}
diff --git a/objectbox/lib/src/native/model.dart b/objectbox/lib/src/native/model.dart
new file mode 100644
index 000000000..f23b515c3
--- /dev/null
+++ b/objectbox/lib/src/native/model.dart
@@ -0,0 +1,112 @@
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart';
+
+import '../common.dart';
+import '../modelinfo/index.dart';
+import 'bindings/bindings.dart';
+import 'bindings/helpers.dart';
+
+// ignore_for_file: public_member_api_docs
+
+class Model {
+ final Pointer _cModel;
+
+ Pointer get ptr => _cModel;
+
+ Model(ModelInfo model)
+ : _cModel = checkObxPtr(C.model(), 'failed to create model') {
+ try {
+ model.entities.forEach(addEntity);
+
+ // set last entity id
+ C.model_last_entity_id(
+ _cModel, model.lastEntityId.id, model.lastEntityId.uid);
+
+ // set last relation id
+ if (model.lastRelationId != null) {
+ C.model_last_relation_id(
+ _cModel, model.lastRelationId.id, model.lastRelationId.uid);
+ }
+
+ // set last index id
+ if (model.lastIndexId != null) {
+ C.model_last_index_id(
+ _cModel, model.lastIndexId.id, model.lastIndexId.uid);
+ }
+ } catch (e) {
+ C.model_free(_cModel);
+ rethrow;
+ }
+ }
+
+ void _check(int errorCode) {
+ if (errorCode == OBX_SUCCESS) return;
+
+ throw ObjectBoxException(
+ dartMsg: 'Model building failed',
+ nativeCode: C.model_error_code(_cModel),
+ nativeMsg: cString(C.model_error_message(_cModel)));
+ }
+
+ void addEntity(ModelEntity entity) {
+ // start entity
+ var name = Utf8.toUtf8(entity.name).cast();
+ try {
+ _check(C.model_entity(_cModel, name, entity.id.id, entity.id.uid));
+ } finally {
+ free(name);
+ }
+
+ if (entity.flags != 0) {
+ // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_model_entity_flags() exists.
+ try {
+ _check(C.model_entity_flags(_cModel, entity.flags));
+ } on ArgumentError {
+ // flags not supported; don't do anything until objectbox-c v0.11
+ // this should only be used from our test code
+ }
+ }
+
+ // add all properties
+ entity.properties.forEach(addProperty);
+
+ // set last property id
+ _check(C.model_entity_last_property_id(
+ _cModel, entity.lastPropertyId.id, entity.lastPropertyId.uid));
+
+ entity.relations.forEach(addRelation);
+ }
+
+ void addProperty(ModelProperty prop) {
+ var name = Utf8.toUtf8(prop.name).cast();
+ try {
+ _check(
+ C.model_property(_cModel, name, prop.type, prop.id.id, prop.id.uid));
+
+ if (prop.isRelation) {
+ var relTarget = Utf8.toUtf8(prop.relationTarget /*!*/).cast();
+ try {
+ _check(C.model_property_relation(_cModel, relTarget,
+ prop.indexId /*!*/ .id, prop.indexId /*!*/ .uid));
+ } finally {
+ free(relTarget);
+ }
+ } else if (prop.indexId != null) {
+ _check(C.model_property_index_id(
+ _cModel, prop.indexId.id, prop.indexId.uid));
+ }
+ } finally {
+ free(name);
+ }
+
+ if (prop.flags != 0) {
+ _check(C.model_property_flags(_cModel, prop.flags));
+ }
+ }
+
+ void addRelation(ModelRelation rel) {
+ _check(C.model_relation(
+ _cModel, rel.id.id, rel.id.uid, rel.targetId.id, rel.targetId.uid));
+ }
+}
diff --git a/objectbox/lib/src/native/observable.dart b/objectbox/lib/src/native/observable.dart
new file mode 100644
index 000000000..e927c8e63
--- /dev/null
+++ b/objectbox/lib/src/native/observable.dart
@@ -0,0 +1,102 @@
+import 'dart:async';
+import 'dart:ffi';
+
+import '../util.dart';
+import 'bindings/bindings.dart';
+import 'query/query.dart';
+import 'store.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);
+ }
+ }
+ }
+
+ static void subscribe(Store store) {
+ syncOrObserversExclusive.mark(store);
+
+ 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);
+ });
+ }
+
+ // #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);
+ }
+
+ static bool isSubscribed(Store store) =>
+ _Observable._anyObserver.containsKey(store.ptr.address);
+}
+
+/// 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);
+ }
+ final storeAddress = store.ptr.address;
+
+ _Observable._any[storeAddress] ??= {};
+ _Observable._any[storeAddress] /*!*/ [entityId] ??= (u, _, __) {
+ // dummy value to trigger an event
+ _Observable.controller.add(entityId);
+ };
+ }
+
+ /// 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();
+ });
+ }
+
+ /// Use this for Query Property
+ Stream> get stream {
+ _setup();
+ return _Observable.controller.stream
+ .where((e) => e == entityId)
+ .map((_) => this);
+ }
+}
diff --git a/objectbox/lib/src/query/builder.dart b/objectbox/lib/src/native/query/builder.dart
similarity index 100%
rename from objectbox/lib/src/query/builder.dart
rename to objectbox/lib/src/native/query/builder.dart
diff --git a/objectbox/lib/src/query/property.dart b/objectbox/lib/src/native/query/property.dart
similarity index 100%
rename from objectbox/lib/src/query/property.dart
rename to objectbox/lib/src/native/query/property.dart
diff --git a/objectbox/lib/src/query/query.dart b/objectbox/lib/src/native/query/query.dart
similarity index 99%
rename from objectbox/lib/src/query/query.dart
rename to objectbox/lib/src/native/query/query.dart
index b8657d958..e722101a0 100644
--- a/objectbox/lib/src/query/query.dart
+++ b/objectbox/lib/src/native/query/query.dart
@@ -5,14 +5,14 @@ import 'dart:typed_data';
import 'package:ffi/ffi.dart' show allocate, free, Utf8;
+import '../../common.dart';
+import '../../modelinfo/entity_definition.dart';
+import '../../store.dart';
+import '../../transaction.dart';
import '../bindings/bindings.dart';
import '../bindings/data_visitor.dart';
import '../bindings/helpers.dart';
import '../bindings/structs.dart';
-import '../common.dart';
-import '../modelinfo/entity_definition.dart';
-import '../store.dart';
-import '../transaction.dart';
part 'builder.dart';
part 'property.dart';
diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart
new file mode 100644
index 000000000..956ce078c
--- /dev/null
+++ b/objectbox/lib/src/native/store.dart
@@ -0,0 +1,166 @@
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart';
+
+import '../common.dart';
+import '../modelinfo/index.dart';
+import '../transaction.dart';
+import '../util.dart';
+import 'bindings/bindings.dart';
+import 'bindings/helpers.dart';
+import 'box.dart';
+import 'model.dart';
+import 'sync.dart';
+
+/// Represents an ObjectBox database and works together with [Box] to allow
+/// getting and putting.
+class Store {
+ /*late final*/ Pointer _cStore;
+ final _boxes = {};
+ final ModelDefinition _defs;
+
+ /// A list of observers of the Store.close() event.
+ final _onClose = {};
+
+ /// Creates a BoxStore using the model definition from the generated
+ /// `objectbox.g.dart` file.
+ ///
+ /// For example in a Flutter app:
+ /// ```dart
+ /// getApplicationDocumentsDirectory().then((dir) {
+ /// _store = Store(getObjectBoxModel(), directory: dir.path + "/objectbox");
+ /// });
+ /// ```
+ ///
+ /// Or for a Dart app:
+ /// ```dart
+ /// var store = Store(getObjectBoxModel());
+ /// ```
+ ///
+ /// See our examples for more details.
+ Store(this._defs,
+ {String /*?*/ directory,
+ int /*?*/ maxDBSizeInKB,
+ int /*?*/ fileMode,
+ int /*?*/ maxReaders}) {
+ var model = Model(_defs.model);
+
+ var opt = C.opt();
+ checkObxPtr(opt, 'failed to create store options');
+
+ try {
+ checkObx(C.opt_model(opt, model.ptr));
+ if (directory != null && directory.isNotEmpty) {
+ var cStr = Utf8.toUtf8(directory).cast();
+ try {
+ checkObx(C.opt_directory(opt, cStr));
+ } finally {
+ free(cStr);
+ }
+ }
+ if (maxDBSizeInKB != null && maxDBSizeInKB > 0) {
+ C.opt_max_db_size_in_kb(opt, maxDBSizeInKB);
+ }
+ if (fileMode != null && fileMode >= 0) {
+ C.opt_file_mode(opt, fileMode);
+ }
+ if (maxReaders != null && maxReaders > 0) {
+ C.opt_max_readers(opt, maxReaders);
+ }
+ } catch (e) {
+ C.opt_free(opt);
+ rethrow;
+ }
+ _cStore = C.store_open(opt);
+
+ try {
+ checkObxPtr(_cStore, 'failed to create store');
+ } on ObjectBoxException catch (e) {
+ // Recognize common problems when trying to open/create a database
+ // 10199 = OBX_ERROR_STORAGE_GENERAL
+ if (e.nativeCode == 10199 &&
+ e.nativeMsg != null &&
+ e.nativeMsg /*!*/ .contains('Dir does not exist')) {
+ // 13 = permissions denied, 30 = read-only filesystem
+ if (e.nativeMsg /*!*/ .endsWith(' (13)') ||
+ e.nativeMsg /*!*/ .endsWith(' (30)')) {
+ final msg = e.nativeMsg /*!*/ +
+ ' - this usually indicates a problem with permissions; '
+ "if you're using Flutter you may need to use "
+ 'getApplicationDocumentsDirectory() from the path_provider '
+ 'package, see example/README.md';
+ throw ObjectBoxException(
+ dartMsg: e.dartMsg, nativeCode: e.nativeCode, nativeMsg: msg);
+ }
+ }
+ rethrow;
+ }
+ }
+
+ /// Closes this store.
+ ///
+ /// Don't try to call any other ObjectBox methods after the store is closed.
+ void close() {
+ _boxes.values.forEach(InternalBoxAccess.close);
+ _boxes.clear();
+
+ // Call each "onClose()" event listener.
+ // Move the list to prevent "Concurrent modification during iteration".
+ _onClose.values.toList(growable: false).forEach((listener) => listener());
+ _onClose.clear();
+
+ checkObx(C.store_close(_cStore));
+ }
+
+ /// Returns a cached Box instance.
+ Box box() {
+ if (!_boxes.containsKey(T)) {
+ return _boxes[T] = InternalBoxAccess.create(this, _entityDef());
+ }
+ return _boxes[T] as Box;
+ }
+
+ EntityDefinition _entityDef() {
+ final binding = _defs.bindings[T];
+ if (binding == null) {
+ throw ArgumentError('Unknown entity type ' + T.toString());
+ }
+ return binding /*!*/ as EntityDefinition;
+ }
+
+ /// Executes a given function inside a transaction. Returns [fn]'s result.
+ /// Aborts a transaction or rethrows if there's an exception.
+ ///
+ /// A transaction can group several operations into a single unit of work that
+ /// either executes completely or not at all.
+ /// The advantage of explicit transactions over the bulk put operations is
+ /// that you can perform any number of operations and use objects of multiple
+ /// boxes. In addition, you get a consistent (transactional) view on your data
+ /// while the transaction is in progress.
+ R runInTransaction(TxMode mode, R Function() fn) =>
+ Transaction.execute(this, mode, fn);
+
+ /// Return an existing SyncClient associated with the store or null if not
+ /// available. Use [Sync.client()] to create one first.
+ SyncClient /*?*/ syncClient() => syncClientsStorage[this];
+
+ /// The low-level pointer to this store.
+ Pointer get ptr => _cStore;
+}
+
+/// Internal only.
+// TODO enable annotation once meta:1.3.0 is out
+// @internal
+class InternalStoreAccess {
+ /// Access entity model for the given class (Dart Type).
+ static EntityDefinition entityDef(Store store) => store._entityDef();
+
+ /// Adds a listener to the [store.close()] event.
+ static void addCloseListener(
+ Store store, dynamic key, void Function() listener) =>
+ store._onClose[key] = listener;
+
+ /// Removes a [store.close()] event listener.
+ static void removeCloseListener(Store store, dynamic key) =>
+ store._onClose.remove(key);
+}
diff --git a/objectbox/lib/src/native/sync.dart b/objectbox/lib/src/native/sync.dart
new file mode 100644
index 000000000..12c9255d5
--- /dev/null
+++ b/objectbox/lib/src/native/sync.dart
@@ -0,0 +1,287 @@
+import 'dart:convert' show utf8;
+import 'dart:ffi';
+import 'dart:typed_data' show Uint8List;
+
+import 'package:ffi/ffi.dart';
+import 'package:meta/meta.dart';
+
+import '../util.dart';
+import 'bindings/bindings.dart';
+import 'bindings/helpers.dart';
+import 'bindings/structs.dart';
+import 'store.dart';
+
+/// Credentials used to authenticate a sync client against a server.
+class SyncCredentials {
+ final int _type;
+ final Uint8List _data;
+
+ SyncCredentials._(this._type, String data)
+ : _data = Uint8List.fromList(utf8.encode(data));
+
+ /// No credentials - usually only for development purposes with a server
+ /// configured to accept all connections without authentication.
+ SyncCredentials.none()
+ : _type = OBXSyncCredentialsType.NONE,
+ _data = Uint8List(0);
+
+ /// Shared secret authentication.
+ SyncCredentials.sharedSecretUint8List(this._data)
+ : _type = OBXSyncCredentialsType.SHARED_SECRET;
+
+ /// Shared secret authentication.
+ SyncCredentials.sharedSecretString(String data)
+ : this._(OBXSyncCredentialsType.SHARED_SECRET, data);
+
+ /// Google authentication.
+ SyncCredentials.googleAuthUint8List(this._data)
+ : _type = OBXSyncCredentialsType.GOOGLE_AUTH;
+
+ /// Google authentication.
+ SyncCredentials.googleAuthString(String data)
+ : this._(OBXSyncCredentialsType.GOOGLE_AUTH, data);
+}
+
+/// Current state of the [SyncClient].
+enum SyncState {
+ /// State is unknown, e.g. C-API reported a state that's not recognized yet.
+ unknown,
+
+ /// Client created but not yet started.
+ created,
+
+ /// Client started and connecting.
+ started,
+
+ /// Connection with the server established but not authenticated yet.
+ connected,
+
+ /// Client authenticated and synchronizing.
+ loggedIn,
+
+ /// Lost connection, will try to reconnect if the credentials are valid.
+ disconnected,
+
+ /// Client in the process of being closed.
+ stopped,
+
+ /// Invalid access to the client after it was closed.
+ dead
+}
+
+/// Configuration of how [SyncClient] fetches remote updates from the server.
+enum SyncRequestUpdatesMode {
+ /// No updates, [SyncClient.requestUpdates()] must be called manually.
+ manual,
+
+ /// Automatic updates, including subsequent pushes from the server, same as
+ /// calling [SyncClient.requestUpdates(true)]. This is the default unless
+ /// changed by [SyncClient.setRequestUpdatesMode()].
+ auto,
+
+ /// Automatic update after connection, without subscribing for pushes from the
+ /// server. Similar to calling [SyncClient.requestUpdates(false)].
+ autoNoPushes
+}
+
+/// Sync client is used to connect to an ObjectBox sync server.
+class SyncClient {
+ final Store _store;
+
+ /*late final*/
+ Pointer _cSync;
+
+ /// The low-level pointer to this box.
+ Pointer get ptr => (_cSync.address != 0)
+ ? _cSync
+ : throw Exception('SyncClient already closed');
+
+ /// Creates a sync client associated with the given store and options.
+ /// This does not initiate any connection attempts yet: call start() to do so.
+ SyncClient(this._store, String serverUri, SyncCredentials creds) {
+ if (!Sync.isAvailable()) {
+ throw Exception(
+ 'Sync is not available in the loaded ObjectBox runtime library. '
+ 'Please visit https://objectbox.io/sync/ for options.');
+ }
+
+ final cServerUri = Utf8.toUtf8(serverUri).cast();
+ try {
+ _cSync = checkObxPtr(
+ C.sync_1(_store.ptr, cServerUri), 'failed to create sync client');
+ } finally {
+ free(cServerUri);
+ }
+
+ setCredentials(creds);
+ }
+
+ /// Closes and cleans up all resources used by this sync client.
+ /// 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() {
+ final err = C.sync_close(_cSync);
+ _cSync = nullptr;
+ syncClientsStorage.remove(_store);
+ InternalStoreAccess.removeCloseListener(_store, this);
+ syncOrObserversExclusive.unmark(_store);
+ checkObx(err);
+ }
+
+ /// Returns if this sync client is closed and can no longer be used.
+ bool isClosed() => _cSync.address == 0;
+
+ /// Gets the current sync client state.
+ SyncState state() {
+ final state = C.sync_state(ptr);
+ switch (state) {
+ case OBXSyncState.CREATED:
+ return SyncState.created;
+ case OBXSyncState.STARTED:
+ return SyncState.started;
+ case OBXSyncState.CONNECTED:
+ return SyncState.connected;
+ case OBXSyncState.LOGGED_IN:
+ return SyncState.loggedIn;
+ case OBXSyncState.DISCONNECTED:
+ return SyncState.disconnected;
+ case OBXSyncState.STOPPED:
+ return SyncState.stopped;
+ case OBXSyncState.DEAD:
+ return SyncState.dead;
+ default:
+ return SyncState.unknown;
+ }
+ }
+
+ /// Configure authentication credentials, depending on your server config.
+ void setCredentials(SyncCredentials creds) {
+ final cCreds = OBX_bytes_wrapper.managedCopyOf(creds._data, align: false);
+ try {
+ checkObx(C.sync_credentials(
+ ptr,
+ creds._type,
+ creds._type == OBXSyncCredentialsType.NONE ? nullptr : cCreds.ptr,
+ cCreds.size));
+ } finally {
+ cCreds.freeManaged();
+ }
+ }
+
+ /// Configures how sync updates are received from the server. If automatic
+ /// updates are turned off, they will need to be requested manually.
+ void setRequestUpdatesMode(SyncRequestUpdatesMode mode) {
+ int cMode;
+ switch (mode) {
+ case SyncRequestUpdatesMode.manual:
+ cMode = OBXRequestUpdatesMode.MANUAL;
+ break;
+ case SyncRequestUpdatesMode.auto:
+ cMode = OBXRequestUpdatesMode.AUTO;
+ break;
+ case SyncRequestUpdatesMode.autoNoPushes:
+ cMode = OBXRequestUpdatesMode.AUTO_NO_PUSHES;
+ break;
+ default:
+ throw Exception('Unknown mode argument: ' + mode.toString());
+ }
+ checkObx(C.sync_request_updates_mode(ptr, cMode));
+ }
+
+ /// Once the sync client is configured, you can [start] it to initiate
+ /// synchronization.
+ ///
+ /// This method triggers communication in the background and returns
+ /// immediately. The background thread will try to connect to the server,
+ /// log-in and start syncing data (depends on [SyncRequestUpdatesMode]).
+ /// If the device, network or server is currently offline, connection attempts
+ /// will be retried later automatically. If you haven't set the credentials in
+ /// the options during construction, call [setCredentials()] before [start()].
+ void start() {
+ checkObx(C.sync_start(ptr));
+ }
+
+ /// Stops this sync client. Does nothing if it is already stopped.
+ void stop() {
+ checkObx(C.sync_stop(ptr));
+ }
+
+ /// Request updates since we last synchronized our database.
+ ///
+ /// Additionally, you can subscribe for future pushes from the server, to let
+ /// it send us future updates as they come in.
+ /// Call [cancelUpdates()] to stop the updates.
+ bool requestUpdates({/*required*/ bool subscribeForFuturePushes}) =>
+ checkObxSuccess(C.sync_updates_request(ptr, subscribeForFuturePushes));
+
+ /// Cancel updates from the server so that it will stop sending updates.
+ /// See also [requestUpdates()].
+ bool cancelUpdates() => checkObxSuccess(C.sync_updates_cancel(ptr));
+
+ /// Count the number of messages in the outgoing queue, i.e. those waiting to
+ /// be sent to the server.
+ ///
+ /// Note: This calls uses a (read) transaction internally:
+ /// 1) It's not just a "cheap" return of a single number. While this will
+ /// still be fast, avoid calling this function excessively.
+ /// 2) the result follows transaction view semantics, thus it may not always
+ /// match the actual value.
+ int outgoingMessageCount({int limit = 0}) {
+ final count = allocate();
+ try {
+ checkObx(C.sync_outgoing_message_count(ptr, limit, count));
+ return count.value;
+ } finally {
+ free(count);
+ }
+ }
+}
+
+/// [ObjectBox Sync](https://objectbox.io/sync/) makes data available and
+/// synchronized across devices, online and offline.
+///
+/// Start a client using [Sync.client()] and connect to a remote server.
+class Sync {
+ /// Create a Sync annotation, enabling synchronization for an entity.
+ const Sync();
+
+ static /*late final*/ bool _syncAvailable;
+
+ /// Returns true if the loaded ObjectBox native library supports Sync.
+ static bool isAvailable() {
+ // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_sync_available() exists.
+ try {
+ _syncAvailable ??= C.sync_available();
+ } catch (_) {
+ _syncAvailable = false;
+ }
+ return _syncAvailable;
+ }
+
+ /// Creates a sync client associated with the given store and configures it
+ /// with the given options. This does not initiate any connection attempts
+ /// yet, call [SyncClient.start()] to do so.
+ ///
+ /// Before [SyncClient.start()], you can still configure some aspects of the
+ /// client, e.g. its [SyncRequestUpdatesMode] mode.
+ static SyncClient client(
+ Store store, String serverUri, SyncCredentials creds) {
+ 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);
+ return client;
+ }
+}
+
+/// Tests only.
+// TODO enable annotation once meta:1.3.0 is out
+// @internal
+@visibleForTesting
+class InternaSyncTestAccess {
+ /// Access credentials internal data representation.
+ static Uint8List credentialsData(SyncCredentials creds) => creds._data;
+}
diff --git a/objectbox/lib/src/native/transaction.dart b/objectbox/lib/src/native/transaction.dart
new file mode 100644
index 000000000..4732ffd30
--- /dev/null
+++ b/objectbox/lib/src/native/transaction.dart
@@ -0,0 +1,110 @@
+import 'dart:ffi';
+
+import '../modelinfo/entity_definition.dart';
+import '../store.dart';
+import '../transaction.dart';
+import 'bindings/bindings.dart';
+import 'bindings/helpers.dart';
+
+// ignore_for_file: public_member_api_docs
+
+// TODO enable annotation once meta:1.3.0 is out
+// @internal
+class Transaction {
+ final Store _store;
+ final bool _isWrite;
+ final Pointer _cTxn;
+ bool _closed = false;
+
+ // We have two ways of keeping cursors because we usually need just one.
+ // The variable is faster then the map initialization & access.
+ /*late final*/
+ CursorHelper _firstCursor;
+
+ /*late final*/
+ Map _cursors;
+
+ Pointer get ptr => _cTxn;
+
+ Transaction(this._store, TxMode mode)
+ : _isWrite = mode == TxMode.write,
+ _cTxn = mode == TxMode.write
+ ? C.txn_write(_store.ptr)
+ : C.txn_read(_store.ptr) {
+ checkObxPtr(_cTxn, 'failed to create transaction');
+ }
+
+ void _finish(bool successful) {
+ if (_isWrite) {
+ try {
+ _mark(successful);
+ } finally {
+ close();
+ }
+ } else {
+ close();
+ }
+ }
+
+ void commitAndClose() => _finish(true);
+
+ void abortAndClose() => _finish(false);
+
+ void _mark(bool successful) =>
+ checkObx(C.txn_mark_success(_cTxn, successful));
+
+ void markSuccessful() => _mark(true);
+
+ void markFailed() => _mark(false);
+
+ void close() {
+ if (_closed) return;
+ _closed = true;
+ if (_firstCursor != null) {
+ _firstCursor.close();
+ if (_cursors != null) {
+ _cursors.values.forEach((c) => c.close());
+ _cursors.clear();
+ }
+ }
+ checkObx(C.txn_close(_cTxn));
+ }
+
+ /// Returns a cursor for the given entity. No need to close it manually.
+ /// Note: the cursor may have already been used, don't rely on its state!
+ CursorHelper cursor(EntityDefinition entity) {
+ if (_firstCursor == null) {
+ return _firstCursor =
+ CursorHelper(_store, _cTxn, entity, isWrite: _isWrite);
+ } else if (_firstCursor.entity == entity) {
+ return _firstCursor as CursorHelper;
+ }
+ _cursors ??= {};
+ final entityId = entity.model.id.id;
+ if (_cursors.containsKey(entityId)) {
+ return _cursors[entityId] as CursorHelper;
+ }
+ return _cursors[entityId] =
+ CursorHelper(_store, _cTxn, entity, isWrite: _isWrite);
+ }
+
+ /// Executes a given function inside a transaction.
+ ///
+ /// Returns type of [fn] if [return] is called in [fn].
+ static R execute(Store store, TxMode mode, R Function() fn) {
+ final tx = Transaction(store, mode);
+ try {
+ // In theory, we should only mark successful after the function finishes.
+ // In practice, it's safe to assume most functions will be successful and
+ // thus marking before the call allows us to return directly, before an
+ // intermediary variable.
+ if (tx._isWrite) tx.markSuccessful();
+ return fn();
+ } catch (ex) {
+ if (tx._isWrite) tx.markFailed();
+ rethrow;
+ } finally {
+ tx.close();
+ }
+ }
+}
diff --git a/objectbox/lib/src/native/version.dart b/objectbox/lib/src/native/version.dart
new file mode 100644
index 000000000..157322f1c
--- /dev/null
+++ b/objectbox/lib/src/native/version.dart
@@ -0,0 +1,22 @@
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart' show allocate, free;
+
+import '../common.dart';
+import 'bindings/bindings.dart';
+
+/// Returns the underlying ObjectBox-C library version.
+Version libraryVersion() {
+ var majorPtr = allocate(),
+ minorPtr = allocate(),
+ patchPtr = allocate();
+
+ try {
+ C.version(majorPtr, minorPtr, patchPtr);
+ return Version(majorPtr.value, minorPtr.value, patchPtr.value);
+ } finally {
+ free(majorPtr);
+ free(minorPtr);
+ free(patchPtr);
+ }
+}
diff --git a/objectbox/lib/src/observable.dart b/objectbox/lib/src/observable.dart
index 2f229f095..0d69e5dd7 100644
--- a/objectbox/lib/src/observable.dart
+++ b/objectbox/lib/src/observable.dart
@@ -1,102 +1 @@
-import 'dart:async';
-import 'dart:ffi';
-
-import 'bindings/bindings.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);
- }
- }
- }
-
- static void subscribe(Store store) {
- syncOrObserversExclusive.mark(store);
-
- 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);
- });
- }
-
- // #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);
- }
-
- static bool isSubscribed(Store store) =>
- _Observable._anyObserver.containsKey(store.ptr.address);
-}
-
-/// 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);
- }
- final storeAddress = store.ptr.address;
-
- _Observable._any[storeAddress] ??= {};
- _Observable._any[storeAddress] /*!*/ [entityId] ??= (u, _, __) {
- // dummy value to trigger an event
- _Observable.controller.add(entityId);
- };
- }
-
- /// 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();
- });
- }
-
- /// Use this for Query Property
- Stream> get stream {
- _setup();
- return _Observable.controller.stream
- .where((e) => e == entityId)
- .map((_) => this);
- }
-}
+export 'native/observable.dart' if (dart.library.html) 'web/observable.dart';
diff --git a/objectbox/lib/src/query.dart b/objectbox/lib/src/query.dart
new file mode 100644
index 000000000..9fcb2b5ca
--- /dev/null
+++ b/objectbox/lib/src/query.dart
@@ -0,0 +1 @@
+export 'native/query/query.dart' if (dart.library.html) 'web/query.dart';
diff --git a/objectbox/lib/src/relations/to_many.dart b/objectbox/lib/src/relations/to_many.dart
index 6230b592d..f2049f686 100644
--- a/objectbox/lib/src/relations/to_many.dart
+++ b/objectbox/lib/src/relations/to_many.dart
@@ -1,10 +1,7 @@
import 'dart:collection';
-import 'dart:ffi';
import 'package:meta/meta.dart';
-import '../bindings/bindings.dart';
-import '../bindings/helpers.dart';
import '../box.dart';
import '../modelinfo/entity_definition.dart';
import '../store.dart';
@@ -177,13 +174,12 @@ class ToMany extends Object with ListMixin {
switch (_rel.type) {
case RelType.toMany:
if (add) {
- if (id == 0) id = _box.put(object, mode: mode);
- checkObx(
- C.box_rel_put(_otherBox.ptr, _rel.id, _rel.objectId, id));
+ if (id == 0) id = InternalBoxAccess.put(_box, object, mode, tx);
+ InternalBoxAccess.relPut(_otherBox, _rel.id, _rel.objectId, id);
} else {
if (id == 0) return;
- checkObx(
- C.box_rel_remove(_otherBox.ptr, _rel.id, _rel.objectId, id));
+ InternalBoxAccess.relRemove(
+ _otherBox, _rel.id, _rel.objectId, id);
}
break;
case RelType.toOneBacklink:
@@ -193,11 +189,11 @@ class ToMany extends Object with ListMixin {
break;
case RelType.toManyBacklink:
if (add) {
- if (id == 0) id = _box.put(object, mode: mode);
- checkObx(C.box_rel_put(_box.ptr, _rel.id, id, _rel.objectId));
+ if (id == 0) id = InternalBoxAccess.put(_box, object, mode, tx);
+ InternalBoxAccess.relPut(_box, _rel.id, id, _rel.objectId);
} else {
if (id == 0) return;
- checkObx(C.box_rel_remove(_box.ptr, _rel.id, id, _rel.objectId));
+ InternalBoxAccess.relRemove(_box, _rel.id, id, _rel.objectId);
}
break;
default:
@@ -229,22 +225,7 @@ class ToMany extends Object with ListMixin {
__items = [];
} else {
_verifyAttached();
- switch (_rel.type) {
- case RelType.toMany:
- __items = _getMany(
- () => C.box_rel_get_ids(_box.ptr, _rel.id, _rel.objectId));
- break;
- case RelType.toOneBacklink:
- __items = _getMany(
- () => C.box_get_backlink_ids(_box.ptr, _rel.id, _rel.objectId));
- break;
- case RelType.toManyBacklink:
- __items = _getMany(() =>
- C.box_rel_get_backlink_ids(_box.ptr, _rel.id, _rel.objectId));
- break;
- default:
- throw UnimplementedError();
- }
+ __items = InternalBoxAccess.getRelated(_box, _rel);
}
if (_addedBeforeLoad.isNotEmpty) {
__items.addAll(_addedBeforeLoad);
@@ -259,36 +240,6 @@ class ToMany extends Object with ListMixin {
"Don't call applyToDb() on new objects, use box.put() instead.");
}
}
-
- /// Similar to box.getMany() but loads the OBX_id_array and reads objects
- /// in a single Transaction, ensuring consistency. And it's a little more
- /// efficient for not unpacking the id array to a dart list.
- List _getMany(Pointer Function() cIdsGetterFn) {
- final tx = Transaction(_store, TxMode.read);
- try {
- final result = [];
- final cIdsPtr = checkObxPtr(cIdsGetterFn());
- try {
- final cIds = cIdsPtr.ref;
- if (cIds.count > 0) {
- final cursor = tx.cursor(_entity);
- for (var i = 0; i < cIds.count; i++) {
- final code = C.cursor_get(
- cursor.ptr, cIds.ids[i], cursor.dataPtrPtr, cursor.sizePtr);
- if (code != OBX_NOT_FOUND) {
- checkObx(code);
- result.add(_entity.objectFromFB(_store, cursor.readData));
- }
- }
- }
- } finally {
- C.id_array_free(cIdsPtr);
- }
- return result;
- } finally {
- tx.close();
- }
- }
}
/// Internal only.
diff --git a/objectbox/lib/src/store.dart b/objectbox/lib/src/store.dart
index 46601d834..7bab24e01 100644
--- a/objectbox/lib/src/store.dart
+++ b/objectbox/lib/src/store.dart
@@ -1,166 +1 @@
-import 'dart:ffi';
-
-import 'package:ffi/ffi.dart';
-
-import 'bindings/bindings.dart';
-import 'bindings/helpers.dart';
-import 'box.dart';
-import 'common.dart';
-import 'model.dart';
-import 'modelinfo/index.dart';
-import 'sync.dart';
-import 'transaction.dart';
-import 'util.dart';
-
-/// Represents an ObjectBox database and works together with [Box] to allow
-/// getting and putting.
-class Store {
- /*late final*/ Pointer _cStore;
- final _boxes = {};
- final ModelDefinition _defs;
-
- /// A list of observers of the Store.close() event.
- final _onClose = {};
-
- /// Creates a BoxStore using the model definition from the generated
- /// `objectbox.g.dart` file.
- ///
- /// For example in a Flutter app:
- /// ```dart
- /// getApplicationDocumentsDirectory().then((dir) {
- /// _store = Store(getObjectBoxModel(), directory: dir.path + "/objectbox");
- /// });
- /// ```
- ///
- /// Or for a Dart app:
- /// ```dart
- /// var store = Store(getObjectBoxModel());
- /// ```
- ///
- /// See our examples for more details.
- Store(this._defs,
- {String /*?*/ directory,
- int /*?*/ maxDBSizeInKB,
- int /*?*/ fileMode,
- int /*?*/ maxReaders}) {
- var model = Model(_defs.model);
-
- var opt = C.opt();
- checkObxPtr(opt, 'failed to create store options');
-
- try {
- checkObx(C.opt_model(opt, model.ptr));
- if (directory != null && directory.isNotEmpty) {
- var cStr = Utf8.toUtf8(directory).cast();
- try {
- checkObx(C.opt_directory(opt, cStr));
- } finally {
- free(cStr);
- }
- }
- if (maxDBSizeInKB != null && maxDBSizeInKB > 0) {
- C.opt_max_db_size_in_kb(opt, maxDBSizeInKB);
- }
- if (fileMode != null && fileMode >= 0) {
- C.opt_file_mode(opt, fileMode);
- }
- if (maxReaders != null && maxReaders > 0) {
- C.opt_max_readers(opt, maxReaders);
- }
- } catch (e) {
- C.opt_free(opt);
- rethrow;
- }
- _cStore = C.store_open(opt);
-
- try {
- checkObxPtr(_cStore, 'failed to create store');
- } on ObjectBoxException catch (e) {
- // Recognize common problems when trying to open/create a database
- // 10199 = OBX_ERROR_STORAGE_GENERAL
- if (e.nativeCode == 10199 &&
- e.nativeMsg != null &&
- e.nativeMsg /*!*/ .contains('Dir does not exist')) {
- // 13 = permissions denied, 30 = read-only filesystem
- if (e.nativeMsg /*!*/ .endsWith(' (13)') ||
- e.nativeMsg /*!*/ .endsWith(' (30)')) {
- final msg = e.nativeMsg /*!*/ +
- ' - this usually indicates a problem with permissions; '
- "if you're using Flutter you may need to use "
- 'getApplicationDocumentsDirectory() from the path_provider '
- 'package, see example/README.md';
- throw ObjectBoxException(
- dartMsg: e.dartMsg, nativeCode: e.nativeCode, nativeMsg: msg);
- }
- }
- rethrow;
- }
- }
-
- /// Closes this store.
- ///
- /// Don't try to call any other ObjectBox methods after the store is closed.
- void close() {
- _boxes.values.forEach(InternalBoxAccess.close);
- _boxes.clear();
-
- // Call each "onClose()" event listener.
- // Move the list to prevent "Concurrent modification during iteration".
- _onClose.values.toList(growable: false).forEach((listener) => listener());
- _onClose.clear();
-
- checkObx(C.store_close(_cStore));
- }
-
- /// Returns a cached Box instance.
- Box box() {
- if (!_boxes.containsKey(T)) {
- return _boxes[T] = InternalBoxAccess.create(this, _entityDef());
- }
- return _boxes[T] as Box;
- }
-
- EntityDefinition _entityDef() {
- final binding = _defs.bindings[T];
- if (binding == null) {
- throw ArgumentError('Unknown entity type ' + T.toString());
- }
- return binding /*!*/ as EntityDefinition;
- }
-
- /// Executes a given function inside a transaction. Returns [fn]'s result.
- /// Aborts a transaction or rethrows if there's an exception.
- ///
- /// A transaction can group several operations into a single unit of work that
- /// either executes completely or not at all.
- /// The advantage of explicit transactions over the bulk put operations is
- /// that you can perform any number of operations and use objects of multiple
- /// boxes. In addition, you get a consistent (transactional) view on your data
- /// while the transaction is in progress.
- R runInTransaction(TxMode mode, R Function() fn) =>
- Transaction.execute(this, mode, fn);
-
- /// Return an existing SyncClient associated with the store or null if not
- /// available. Use [Sync.client()] to create one first.
- SyncClient /*?*/ syncClient() => syncClientsStorage[this];
-
- /// The low-level pointer to this store.
- Pointer get ptr => _cStore;
-}
-
-/// Internal only.
-// TODO enable annotation once meta:1.3.0 is out
-// @internal
-class InternalStoreAccess {
- /// Access entity model for the given class (Dart Type).
- static EntityDefinition entityDef(Store store) => store._entityDef();
-
- /// Adds a listener to the [store.close()] event.
- static void addCloseListener(
- Store store, dynamic key, void Function() listener) =>
- store._onClose[key] = listener;
-
- /// Removes a [store.close()] event listener.
- static void removeCloseListener(Store store, dynamic key) =>
- store._onClose.remove(key);
-}
+export 'native/store.dart' if (dart.library.html) 'web/store.dart';
diff --git a/objectbox/lib/src/sync.dart b/objectbox/lib/src/sync.dart
index 0095fd8c8..9f9d9ce3a 100644
--- a/objectbox/lib/src/sync.dart
+++ b/objectbox/lib/src/sync.dart
@@ -1,287 +1 @@
-import 'dart:convert' show utf8;
-import 'dart:ffi';
-import 'dart:typed_data' show Uint8List;
-
-import 'package:ffi/ffi.dart';
-import 'package:meta/meta.dart';
-
-import 'bindings/bindings.dart';
-import 'bindings/helpers.dart';
-import 'bindings/structs.dart';
-import 'store.dart';
-import 'util.dart';
-
-/// Credentials used to authenticate a sync client against a server.
-class SyncCredentials {
- final int _type;
- final Uint8List _data;
-
- SyncCredentials._(this._type, String data)
- : _data = Uint8List.fromList(utf8.encode(data));
-
- /// No credentials - usually only for development purposes with a server
- /// configured to accept all connections without authentication.
- SyncCredentials.none()
- : _type = OBXSyncCredentialsType.NONE,
- _data = Uint8List(0);
-
- /// Shared secret authentication.
- SyncCredentials.sharedSecretUint8List(this._data)
- : _type = OBXSyncCredentialsType.SHARED_SECRET;
-
- /// Shared secret authentication.
- SyncCredentials.sharedSecretString(String data)
- : this._(OBXSyncCredentialsType.SHARED_SECRET, data);
-
- /// Google authentication.
- SyncCredentials.googleAuthUint8List(this._data)
- : _type = OBXSyncCredentialsType.GOOGLE_AUTH;
-
- /// Google authentication.
- SyncCredentials.googleAuthString(String data)
- : this._(OBXSyncCredentialsType.GOOGLE_AUTH, data);
-}
-
-/// Current state of the [SyncClient].
-enum SyncState {
- /// State is unknown, e.g. C-API reported a state that's not recognized yet.
- unknown,
-
- /// Client created but not yet started.
- created,
-
- /// Client started and connecting.
- started,
-
- /// Connection with the server established but not authenticated yet.
- connected,
-
- /// Client authenticated and synchronizing.
- loggedIn,
-
- /// Lost connection, will try to reconnect if the credentials are valid.
- disconnected,
-
- /// Client in the process of being closed.
- stopped,
-
- /// Invalid access to the client after it was closed.
- dead
-}
-
-/// Configuration of how [SyncClient] fetches remote updates from the server.
-enum SyncRequestUpdatesMode {
- /// No updates, [SyncClient.requestUpdates()] must be called manually.
- manual,
-
- /// Automatic updates, including subsequent pushes from the server, same as
- /// calling [SyncClient.requestUpdates(true)]. This is the default unless
- /// changed by [SyncClient.setRequestUpdatesMode()].
- auto,
-
- /// Automatic update after connection, without subscribing for pushes from the
- /// server. Similar to calling [SyncClient.requestUpdates(false)].
- autoNoPushes
-}
-
-/// Sync client is used to connect to an ObjectBox sync server.
-class SyncClient {
- final Store _store;
-
- /*late final*/
- Pointer _cSync;
-
- /// The low-level pointer to this box.
- Pointer get ptr => (_cSync.address != 0)
- ? _cSync
- : throw Exception('SyncClient already closed');
-
- /// Creates a sync client associated with the given store and options.
- /// This does not initiate any connection attempts yet: call start() to do so.
- SyncClient(this._store, String serverUri, SyncCredentials creds) {
- if (!Sync.isAvailable()) {
- throw Exception(
- 'Sync is not available in the loaded ObjectBox runtime library. '
- 'Please visit https://objectbox.io/sync/ for options.');
- }
-
- final cServerUri = Utf8.toUtf8(serverUri).cast();
- try {
- _cSync = checkObxPtr(
- C.sync_1(_store.ptr, cServerUri), 'failed to create sync client');
- } finally {
- free(cServerUri);
- }
-
- setCredentials(creds);
- }
-
- /// Closes and cleans up all resources used by this sync client.
- /// 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() {
- final err = C.sync_close(_cSync);
- _cSync = nullptr;
- syncClientsStorage.remove(_store);
- InternalStoreAccess.removeCloseListener(_store, this);
- syncOrObserversExclusive.unmark(_store);
- checkObx(err);
- }
-
- /// Returns if this sync client is closed and can no longer be used.
- bool isClosed() => _cSync.address == 0;
-
- /// Gets the current sync client state.
- SyncState state() {
- final state = C.sync_state(ptr);
- switch (state) {
- case OBXSyncState.CREATED:
- return SyncState.created;
- case OBXSyncState.STARTED:
- return SyncState.started;
- case OBXSyncState.CONNECTED:
- return SyncState.connected;
- case OBXSyncState.LOGGED_IN:
- return SyncState.loggedIn;
- case OBXSyncState.DISCONNECTED:
- return SyncState.disconnected;
- case OBXSyncState.STOPPED:
- return SyncState.stopped;
- case OBXSyncState.DEAD:
- return SyncState.dead;
- default:
- return SyncState.unknown;
- }
- }
-
- /// Configure authentication credentials, depending on your server config.
- void setCredentials(SyncCredentials creds) {
- final cCreds = OBX_bytes_wrapper.managedCopyOf(creds._data, align: false);
- try {
- checkObx(C.sync_credentials(
- ptr,
- creds._type,
- creds._type == OBXSyncCredentialsType.NONE ? nullptr : cCreds.ptr,
- cCreds.size));
- } finally {
- cCreds.freeManaged();
- }
- }
-
- /// Configures how sync updates are received from the server. If automatic
- /// updates are turned off, they will need to be requested manually.
- void setRequestUpdatesMode(SyncRequestUpdatesMode mode) {
- int cMode;
- switch (mode) {
- case SyncRequestUpdatesMode.manual:
- cMode = OBXRequestUpdatesMode.MANUAL;
- break;
- case SyncRequestUpdatesMode.auto:
- cMode = OBXRequestUpdatesMode.AUTO;
- break;
- case SyncRequestUpdatesMode.autoNoPushes:
- cMode = OBXRequestUpdatesMode.AUTO_NO_PUSHES;
- break;
- default:
- throw Exception('Unknown mode argument: ' + mode.toString());
- }
- checkObx(C.sync_request_updates_mode(ptr, cMode));
- }
-
- /// Once the sync client is configured, you can [start] it to initiate
- /// synchronization.
- ///
- /// This method triggers communication in the background and returns
- /// immediately. The background thread will try to connect to the server,
- /// log-in and start syncing data (depends on [SyncRequestUpdatesMode]).
- /// If the device, network or server is currently offline, connection attempts
- /// will be retried later automatically. If you haven't set the credentials in
- /// the options during construction, call [setCredentials()] before [start()].
- void start() {
- checkObx(C.sync_start(ptr));
- }
-
- /// Stops this sync client. Does nothing if it is already stopped.
- void stop() {
- checkObx(C.sync_stop(ptr));
- }
-
- /// Request updates since we last synchronized our database.
- ///
- /// Additionally, you can subscribe for future pushes from the server, to let
- /// it send us future updates as they come in.
- /// Call [cancelUpdates()] to stop the updates.
- bool requestUpdates({/*required*/ bool subscribeForFuturePushes}) =>
- checkObxSuccess(C.sync_updates_request(ptr, subscribeForFuturePushes));
-
- /// Cancel updates from the server so that it will stop sending updates.
- /// See also [requestUpdates()].
- bool cancelUpdates() => checkObxSuccess(C.sync_updates_cancel(ptr));
-
- /// Count the number of messages in the outgoing queue, i.e. those waiting to
- /// be sent to the server.
- ///
- /// Note: This calls uses a (read) transaction internally:
- /// 1) It's not just a "cheap" return of a single number. While this will
- /// still be fast, avoid calling this function excessively.
- /// 2) the result follows transaction view semantics, thus it may not always
- /// match the actual value.
- int outgoingMessageCount({int limit = 0}) {
- final count = allocate();
- try {
- checkObx(C.sync_outgoing_message_count(ptr, limit, count));
- return count.value;
- } finally {
- free(count);
- }
- }
-}
-
-/// [ObjectBox Sync](https://objectbox.io/sync/) makes data available and
-/// synchronized across devices, online and offline.
-///
-/// Start a client using [Sync.client()] and connect to a remote server.
-class Sync {
- /// Create a Sync annotation, enabling synchronization for an entity.
- const Sync();
-
- static /*late final*/ bool _syncAvailable;
-
- /// Returns true if the loaded ObjectBox native library supports Sync.
- static bool isAvailable() {
- // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_sync_available() exists.
- try {
- _syncAvailable ??= C.sync_available();
- } catch (_) {
- _syncAvailable = false;
- }
- return _syncAvailable;
- }
-
- /// Creates a sync client associated with the given store and configures it
- /// with the given options. This does not initiate any connection attempts
- /// yet, call [SyncClient.start()] to do so.
- ///
- /// Before [SyncClient.start()], you can still configure some aspects of the
- /// client, e.g. its [SyncRequestUpdatesMode] mode.
- static SyncClient client(
- Store store, String serverUri, SyncCredentials creds) {
- 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);
- return client;
- }
-}
-
-/// Tests only.
-// TODO enable annotation once meta:1.3.0 is out
-// @internal
-@visibleForTesting
-class InternaSyncTestAccess {
- /// Access credentials internal data representation.
- static Uint8List credentialsData(SyncCredentials creds) => creds._data;
-}
+export 'native/sync.dart' if (dart.library.html) 'web/sync.dart';
diff --git a/objectbox/lib/src/transaction.dart b/objectbox/lib/src/transaction.dart
index d83eaa111..07e8e5f55 100644
--- a/objectbox/lib/src/transaction.dart
+++ b/objectbox/lib/src/transaction.dart
@@ -1,11 +1,4 @@
-import 'dart:ffi';
-
-import 'bindings/bindings.dart';
-import 'bindings/helpers.dart';
-import 'modelinfo/entity_definition.dart';
-import 'store.dart';
-
-// ignore_for_file: public_member_api_docs
+export 'native/transaction.dart' if (dart.library.html) 'web/transaction.dart';
/// Configure transaction mode. Used with [Store.runInTransaction()].
enum TxMode {
@@ -21,104 +14,3 @@ enum TxMode {
/// write data to the disk at the end.
write,
}
-
-// TODO enable annotation once meta:1.3.0 is out
-// @internal
-class Transaction {
- final Store _store;
- final bool _isWrite;
- final Pointer _cTxn;
- bool _closed = false;
-
- // We have two ways of keeping cursors because we usually need just one.
- // The variable is faster then the map initialization & access.
- /*late final*/
- CursorHelper _firstCursor;
-
- /*late final*/
- Map _cursors;
-
- Pointer get ptr => _cTxn;
-
- Transaction(this._store, TxMode mode)
- : _isWrite = mode == TxMode.write,
- _cTxn = mode == TxMode.write
- ? C.txn_write(_store.ptr)
- : C.txn_read(_store.ptr) {
- checkObxPtr(_cTxn, 'failed to create transaction');
- }
-
- void _finish(bool successful) {
- if (_isWrite) {
- try {
- _mark(successful);
- } finally {
- close();
- }
- } else {
- close();
- }
- }
-
- void commitAndClose() => _finish(true);
-
- void abortAndClose() => _finish(false);
-
- void _mark(bool successful) =>
- checkObx(C.txn_mark_success(_cTxn, successful));
-
- void markSuccessful() => _mark(true);
-
- void markFailed() => _mark(false);
-
- void close() {
- if (_closed) return;
- _closed = true;
- if (_firstCursor != null) {
- _firstCursor.close();
- if (_cursors != null) {
- _cursors.values.forEach((c) => c.close());
- _cursors.clear();
- }
- }
- checkObx(C.txn_close(_cTxn));
- }
-
- /// Returns a cursor for the given entity. No need to close it manually.
- /// Note: the cursor may have already been used, don't rely on its state!
- CursorHelper cursor(EntityDefinition entity) {
- if (_firstCursor == null) {
- return _firstCursor =
- CursorHelper(_store, _cTxn, entity, isWrite: _isWrite);
- } else if (_firstCursor.entity == entity) {
- return _firstCursor as CursorHelper;
- }
- _cursors ??= {};
- final entityId = entity.model.id.id;
- if (_cursors.containsKey(entityId)) {
- return _cursors[entityId] as CursorHelper;
- }
- return _cursors[entityId] =
- CursorHelper(_store, _cTxn, entity, isWrite: _isWrite);
- }
-
- /// Executes a given function inside a transaction.
- ///
- /// Returns type of [fn] if [return] is called in [fn].
- static R execute(Store store, TxMode mode, R Function() fn) {
- final tx = Transaction(store, mode);
- try {
- // In theory, we should only mark successful after the function finishes.
- // In practice, it's safe to assume most functions will be successful and
- // thus marking before the call allows us to return directly, before an
- // intermediary variable.
- if (tx._isWrite) tx.markSuccessful();
- return fn();
- } catch (ex) {
- if (tx._isWrite) tx.markFailed();
- rethrow;
- } finally {
- tx.close();
- }
- }
-}
diff --git a/objectbox/lib/src/web/box.dart b/objectbox/lib/src/web/box.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/box.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/lib/src/web/model.dart b/objectbox/lib/src/web/model.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/model.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/lib/src/web/observable.dart b/objectbox/lib/src/web/observable.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/observable.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/lib/src/web/query.dart b/objectbox/lib/src/web/query.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/query.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/lib/src/web/store.dart b/objectbox/lib/src/web/store.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/store.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/lib/src/web/sync.dart b/objectbox/lib/src/web/sync.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/sync.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/lib/src/web/transaction.dart b/objectbox/lib/src/web/transaction.dart
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/objectbox/lib/src/web/transaction.dart
@@ -0,0 +1 @@
+
diff --git a/objectbox/test/basics_test.dart b/objectbox/test/basics_test.dart
index b60f3b5c8..c882703d8 100644
--- a/objectbox/test/basics_test.dart
+++ b/objectbox/test/basics_test.dart
@@ -1,7 +1,7 @@
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/native/bindings/bindings.dart';
+import 'package:objectbox/src/native/bindings/helpers.dart';
import 'package:test/test.dart';
void main() {
diff --git a/objectbox/test/flatbuffers_test.dart b/objectbox/test/flatbuffers_test.dart
index 2815a44e7..de46fd81e 100644
--- a/objectbox/test/flatbuffers_test.dart
+++ b/objectbox/test/flatbuffers_test.dart
@@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:flat_buffers/flat_buffers.dart' as fb_upstream;
-import 'package:objectbox/src/bindings/flatbuffers.dart';
+import 'package:objectbox/src/native/bindings/flatbuffers.dart';
Uint8List addFbData(dynamic fbb) {
fbb.startTable();
diff --git a/objectbox/test/observer_test.dart b/objectbox/test/observer_test.dart
index 4d5eea4d0..d1a3c525f 100644
--- a/objectbox/test/observer_test.dart
+++ b/objectbox/test/observer_test.dart
@@ -1,6 +1,6 @@
import 'dart:ffi';
-import 'package:objectbox/src/bindings/bindings.dart';
+import 'package:objectbox/src/native/bindings/bindings.dart';
import 'package:test/test.dart';
import 'entity.dart';
diff --git a/objectbox/test/sync_test.dart b/objectbox/test/sync_test.dart
index f3fadf227..5ae98e1d5 100644
--- a/objectbox/test/sync_test.dart
+++ b/objectbox/test/sync_test.dart
@@ -1,7 +1,7 @@
import 'dart:math';
import 'dart:typed_data';
-import 'package:objectbox/src/bindings/bindings.dart';
+import 'package:objectbox/src/modelinfo/enums.dart';
import 'package:objectbox/objectbox.dart';
import 'package:objectbox/internal.dart';
import 'package:test/test.dart';