@@ -9,6 +9,7 @@ import 'dart:typed_data';
99
1010import  'package:ffi/ffi.dart' ;
1111import  'package:meta/meta.dart' ;
12+ import  'package:objectbox/src/native/version.dart' ;
1213import  'package:path/path.dart'  as  path;
1314
1415import  '../common.dart' ;
@@ -27,6 +28,13 @@ part 'observable.dart';
2728/// Represents an ObjectBox database and works together with [Box]  to allow 
2829/// getting and putting. 
2930class  Store  {
31+   /// Path of the default directory, currently 'objectbox'. 
32+    static  const  String  defaultDirectoryPath =  'objectbox' ;
33+ 
34+   /// Enables a couple of debug logs. 
35+   /// This meant for tests only; do not enable for releases! 
36+    static  bool  debugLogs =  false ;
37+ 
3038  late  final  Pointer <OBX_store > _cStore;
3139  HashMap <int , Type >?  _entityTypeById;
3240  final  _boxes =  HashMap <Type , Box >();
@@ -36,8 +44,8 @@ class Store {
3644  final  _reader =  ReaderWithCBuffer ();
3745  Transaction ?  _tx;
3846
39-   /// absolute  path to the database directory 
40-    final  String  _dbDir ;
47+   /// Absolute  path to the database directory, used for open check.  
48+    final  String  _absoluteDirectoryPath ;
4149
4250  late  final  ByteData  _reference;
4351
@@ -51,8 +59,12 @@ class Store {
5159  /// Default value for string query conditions [caseSensitive]  argument. 
5260   final  bool  _queriesCaseSensitiveDefault;
5361
62+   static  String  _safeDirectoryPath (String ?  path) => 
63+       (path ==  null  ||  path.isEmpty) ?  defaultDirectoryPath :  path;
64+ 
5465  /// Creates a BoxStore using the model definition from your 
55-   /// `objectbox.g.dart`  file. 
66+   /// `objectbox.g.dart`  file in the given [directory]  path 
67+   /// (or if null the [defaultDirectoryPath] ). 
5668  /// 
5769  /// For example in a Flutter app: 
5870  /// ```dart 
@@ -76,10 +88,8 @@ class Store {
7688      String ?  macosApplicationGroup})
7789      :  _weak =  false ,
7890        _queriesCaseSensitiveDefault =  queriesCaseSensitiveDefault,
79-         _dbDir =  path.context.canonicalize (
80-             (directory ==  null  ||  directory.isEmpty)
81-                 ?  'objectbox' 
82-                 :  directory) {
91+         _absoluteDirectoryPath = 
92+             path.context.canonicalize (_safeDirectoryPath (directory)) {
8393    try  {
8494      if  (Platform .isMacOS &&  macosApplicationGroup !=  null ) {
8595        if  (! macosApplicationGroup.endsWith ('/' )) {
@@ -96,12 +106,7 @@ class Store {
96106          malloc.free (cStr);
97107        }
98108      }
99-       if  (_openStoreDirectories.contains (_dbDir)) {
100-         throw  UnsupportedError (
101-             'Cannot create multiple Store instances for the same directory. ' 
102-             'Please use a single Store or close() the previous instance before ' 
103-             'opening another one.' );
104-       }
109+       _checkStoreDirectoryNotOpen ();
105110      final  model =  Model (_defs.model);
106111
107112      final  opt =  C .opt ();
@@ -130,25 +135,14 @@ class Store {
130135        C .opt_free (opt);
131136        rethrow ;
132137      }
138+       if  (debugLogs) {
139+         print ('Opening store (C lib V${libraryVersion ()})... path=$directory ' 
140+             ' isOpen=${isOpen (directory )}' );
141+       }
142+ 
133143      _cStore =  C .store_open (opt);
134144
135-       try  {
136-         checkObxPtr (_cStore, 'failed to create store' );
137-       } on  ObjectBoxException  catch  (e) {
138-         // Recognize common problems when trying to open/create a database 
139-         // 10199 = OBX_ERROR_STORAGE_GENERAL 
140-         // 13 = permissions denied, 30 = read-only filesystem 
141-         if  (e.message.contains (OBX_ERROR_STORAGE_GENERAL .toString ()) && 
142-             e.message.contains ('Dir does not exist' ) && 
143-             (e.message.endsWith (' (13)' ) ||  e.message.endsWith (' (30)' ))) {
144-           throw  ObjectBoxException (e.message + 
145-               ' - this usually indicates a problem with permissions; ' 
146-                   "if you're using Flutter you may need to use " 
147-                   'getApplicationDocumentsDirectory() from the path_provider ' 
148-                   'package, see example/README.md' );
149-         }
150-         rethrow ;
151-       }
145+       _checkStorePointer (_cStore);
152146
153147      // Always create _reference, so it can be non-nullable. 
154148      // Ensure we only try to access the store created in the same process. 
@@ -157,7 +151,7 @@ class Store {
157151      _reference.setUint64 (0  *  _int64Size, pid);
158152      _reference.setUint64 (1  *  _int64Size, _ptr.address);
159153
160-       _openStoreDirectories.add (_dbDir );
154+       _openStoreDirectories.add (_absoluteDirectoryPath );
161155    } catch  (e) {
162156      _reader.clear ();
163157      rethrow ;
@@ -205,7 +199,7 @@ class Store {
205199      {bool  queriesCaseSensitiveDefault =  true })
206200      // must not close the same native store twice so [_weak]=true 
207201      :  _weak =  true ,
208-         _dbDir  =  '' ,
202+         _absoluteDirectoryPath  =  '' ,
209203        _queriesCaseSensitiveDefault =  queriesCaseSensitiveDefault {
210204    // see [reference] for serialization order 
211205    final  readPid =  _reference.getUint64 (0  *  _int64Size);
@@ -221,6 +215,95 @@ class Store {
221215    }
222216  }
223217
218+   /// Attach to a store opened in the [directoryPath]  
219+   /// (or if null the [defaultDirectoryPath] ). 
220+   /// 
221+   /// Use this to access an open store from other isolates. 
222+   /// This results in each isolate having access to the same underlying native 
223+   /// store. 
224+   /// 
225+   /// The returned store is a new instance (e.g. different pointer value) with 
226+   /// its own lifetime and must also be closed (e.g. before an isolate exits). 
227+   /// The actual underlying store is only closed when the last store instance 
228+   /// is closed (e.g. when the app exits). 
229+    Store .attach (this ._defs, String ?  directoryPath,
230+       {bool  queriesCaseSensitiveDefault =  true })
231+       // _weak = false so store can be closed. 
232+       :  _weak =  false ,
233+         _queriesCaseSensitiveDefault =  queriesCaseSensitiveDefault,
234+         _absoluteDirectoryPath = 
235+             path.context.canonicalize (_safeDirectoryPath (directoryPath)) {
236+     try  {
237+       // Do not allow attaching to a store that is already open in the current 
238+       // isolate. While technically possible this is not the intended usage 
239+       // and e.g. transactions would have to be carefully managed to not 
240+       // overlap. 
241+       _checkStoreDirectoryNotOpen ();
242+ 
243+       final  path =  _safeDirectoryPath (directoryPath);
244+       final  pathCStr =  path.toNativeUtf8 ();
245+       try  {
246+         if  (debugLogs) {
247+           final  isOpen =  C .store_is_open (pathCStr.cast ());
248+           print ('Attaching to store... path=$path  isOpen=$isOpen ' );
249+         }
250+         _cStore =  C .store_attach (pathCStr.cast ());
251+       } finally  {
252+         malloc.free (pathCStr);
253+       }
254+ 
255+       checkObxPtr (_cStore,
256+           'could not attach to the store at given path - please ensure it was opened before' );
257+ 
258+       // Not setting _reference as this is a replacement for obtaining a store 
259+       // via reference. 
260+     } catch  (e) {
261+       _reader.clear ();
262+       rethrow ;
263+     }
264+   }
265+ 
266+   void  _checkStoreDirectoryNotOpen () {
267+     if  (_openStoreDirectories.contains (_absoluteDirectoryPath)) {
268+       throw  UnsupportedError (
269+           'Cannot create multiple Store instances for the same directory in the same isolate. ' 
270+           'Please use a single Store, close() the previous instance before ' 
271+           'opening another one or attach to it in another isolate.' );
272+     }
273+   }
274+ 
275+   void  _checkStorePointer (Pointer  cStore) {
276+     try  {
277+       checkObxPtr (cStore, 'failed to create store' );
278+     } on  ObjectBoxException  catch  (e) {
279+       // Recognize common problems when trying to open/create a database 
280+       // 10199 = OBX_ERROR_STORAGE_GENERAL 
281+       // 13 = permissions denied, 30 = read-only filesystem 
282+       if  (e.message.contains (OBX_ERROR_STORAGE_GENERAL .toString ()) && 
283+           e.message.contains ('Dir does not exist' ) && 
284+           (e.message.endsWith (' (13)' ) ||  e.message.endsWith (' (30)' ))) {
285+         throw  ObjectBoxException (e.message + 
286+             ' - this usually indicates a problem with permissions; ' 
287+                 "if you're using Flutter you may need to use " 
288+                 'getApplicationDocumentsDirectory() from the path_provider ' 
289+                 'package, see example/README.md' );
290+       }
291+       rethrow ;
292+     }
293+   }
294+ 
295+   /// Returns if an open store (i.e. opened before and not yet closed) was found 
296+   /// for the given [directoryPath]  (or if null the [defaultDirectoryPath] ). 
297+    static  bool  isOpen (String ?  directoryPath) {
298+     final  path =  _safeDirectoryPath (directoryPath);
299+     final  cStr =  path.toNativeUtf8 ();
300+     try  {
301+       return  C .store_is_open (cStr.cast ());
302+     } finally  {
303+       malloc.free (cStr);
304+     }
305+   }
306+ 
224307  /// Returns a store reference you can use to create a new store instance with 
225308  /// a single underlying native store. See [Store.fromReference]  for more details. 
226309   ByteData  get  reference =>  _reference;
@@ -243,7 +326,7 @@ class Store {
243326    _reader.clear ();
244327
245328    if  (! _weak) {
246-       _openStoreDirectories.remove (_dbDir );
329+       _openStoreDirectories.remove (_absoluteDirectoryPath );
247330      checkObx (C .store_close (_cStore));
248331    }
249332  }
0 commit comments