#
Custom Storage Modules
The beauty of Nitrite is that it is highly extensible. You can write your own storage module and use it with Nitrite. Nitrite provides a simple interface StoreModule
to write your own storage module.
#
StoreModule Interface
To write a custom storage module, you need to implement the StoreModule
interface. The interface has only one method getStore()
which returns an implementation of NitriteStore
interface.
Optionally, you can also implement the StoreConfig
interface to provide configuration options for your storage module.
#
NitriteStore Interface
The NitriteStore
is the storage abstraction layer for Nitrite. It provides the basic operations required by Nitrite to store and retrieve data. A NitriteStore
is responsible for managing NitriteMap
and NitriteRTree
instances which are the main building blocks of database collections.
#
StoreConfig Interface
The StoreConfig
interface provides configuration options for your storage module. You can use this interface to provide configuration options for your storage module.
#
NitriteMap Interface
The NitriteMap
interface is the abstraction layer for Nitrite's key-value store. It provides the basic operations required by a collection to store and retrieve data. A NitriteMap
is responsible for managing NitriteId
and Document
instances which are the main unit of data storage in Nitrite.
#
NitriteRTree Interface
The NitriteRTree
interface is the abstraction layer for Nitrite's R-Tree. It provides the basic operations required by a spatial index to store and retrieve data.
#
Example
Let's write a simple storage module which stores data in a Map
object. The module will be a simple in-memory storage module. The module will be configured with a Map
object and will use it to store data.
#
Store Module
The following code snippet shows the implementation of the storage module.
import 'package:nitrite/nitrite.dart';
class InMemoryStoreModule extends StoreModule {
InMemoryConfig _storeConfig = InMemoryConfig();
static InMemoryModuleBuilder withConfig() => InMemoryModuleBuilder._();
@override
NitriteStore<Config> getStore<Config extends StoreConfig>() {
var store = InMemoryStore(_storeConfig);
return store as NitriteStore<Config>;
}
@override
Set<NitritePlugin> get plugins => <NitritePlugin>{getStore()};
}
#
Store Config
The following code snippet shows the implementation of the store configuration.
class InMemoryConfig extends StoreConfig {
Set<StoreEventListener> _eventListeners = <StoreEventListener>{};
@override
void addStoreEventListener(StoreEventListener listener) {
_eventListeners.add(listener);
}
@override
String? get filePath => null;
@override
bool get isReadOnly => false;
@override
Set<StoreEventListener> get eventListeners =>
Set.unmodifiable(_eventListeners);
}
And the builder class for the configuration.
class InMemoryModuleBuilder {
final Set<StoreEventListener> _eventListeners = <StoreEventListener>{};
final InMemoryConfig _dbConfig = InMemoryConfig();
InMemoryModuleBuilder._();
/// Adds a [StoreEventListener] to the in-memory module builder.
InMemoryModuleBuilder addStoreEventListener(StoreEventListener listener) {
_eventListeners.add(listener);
return this;
}
/// Builds an in-memory store module.
InMemoryStoreModule build() {
var module = InMemoryStoreModule();
_dbConfig._eventListeners = _eventListeners;
module._storeConfig = _dbConfig;
return module;
}
}
#
Nitrite Store
The following code snippet shows the implementation of the NitriteStore
interface.
class InMemoryStore extends AbstractNitriteStore<InMemoryConfig> {
final Map<String, dynamic> _nitriteMapRegistry;
final Map<String, dynamic> _nitriteRTreeMapRegistry;
bool _closed = false;
InMemoryStore(super._storeConfig)
: _nitriteMapRegistry = <String, dynamic>{},
_nitriteRTreeMapRegistry = <String, dynamic>{};
@override
bool get isClosed => _closed;
@override
Future<bool> get hasUnsavedChanges async => false;
@override
bool get isReadOnly => false;
@override
String get storeVersion => 'InMemory/$nitriteVersion';
@override
Future<void> openOrCreate() async {
_initEventBus();
alert(StoreEvents.opened);
}
@override
Future<void> close() async {
// close all maps and rtree
close(MapEntry entry) => entry.value.close();
_closed = true;
// to avoid concurrent modification exception
var tempMap = Map.from(_nitriteMapRegistry);
tempMap.entries.forEach(close);
tempMap = Map.from(_nitriteRTreeMapRegistry);
tempMap.entries.forEach(close);
_nitriteMapRegistry.clear();
_nitriteRTreeMapRegistry.clear();
await super.close();
}
@override
Future<void> commit() async {
alert(StoreEvents.commit);
}
@override
Future<bool> hasMap(String mapName) async {
return _nitriteMapRegistry.containsKey(mapName) ||
_nitriteRTreeMapRegistry.containsKey(mapName);
}
@override
Future<NitriteMap<Key, Value>> openMap<Key, Value>(String mapName) async {
if (_nitriteMapRegistry.containsKey(mapName)) {
var nitriteMap = _nitriteMapRegistry[mapName]!;
if (nitriteMap.isClosed) {
_nitriteMapRegistry.remove(mapName);
} else {
return nitriteMap as InMemoryMap<Key, Value>;
}
}
var nitriteMap = InMemoryMap<Key, Value>(mapName, this);
_nitriteMapRegistry[mapName] = nitriteMap;
return nitriteMap;
}
@override
Future<NitriteRTree<Key, Value>> openRTree<Key extends BoundingBox, Value>(
String rTreeName) async {
if (_nitriteRTreeMapRegistry.containsKey(rTreeName)) {
return _nitriteRTreeMapRegistry[rTreeName]! as InMemoryRTree<Key, Value>;
}
var nitriteRTree = InMemoryRTree<Key, Value>(rTreeName, this);
_nitriteRTreeMapRegistry[rTreeName] = nitriteRTree;
return nitriteRTree;
}
@override
Future<void> closeMap(String mapName) async {
_nitriteMapRegistry.remove(mapName);
}
@override
Future<void> closeRTree(String rTreeName) async {
_nitriteRTreeMapRegistry.remove(rTreeName);
}
@override
Future<void> removeMap(String mapName) async {
if (_nitriteMapRegistry.containsKey(mapName)) {
var map = _nitriteMapRegistry[mapName]!;
if (!map.isClosed && !map.isDropped) {
await map.close();
}
_nitriteMapRegistry.remove(mapName);
var catalog = await getCatalog();
await catalog.remove(mapName);
}
}
@override
Future<void> removeRTree(String rTreeName) async {
if (_nitriteRTreeMapRegistry.containsKey(rTreeName)) {
var rTree = _nitriteRTreeMapRegistry[rTreeName]!;
await rTree.close();
_nitriteRTreeMapRegistry.remove(rTreeName);
var catalog = await getCatalog();
await catalog.remove(rTreeName);
}
}
void _initEventBus() {
if (storeConfig!.eventListeners.isNotEmpty) {
for (var listener in storeConfig!.eventListeners) {
subscribe(listener);
}
}
}
}
#
Nitrite Map
The following code snippet shows the implementation of the NitriteMap
interface.
class InMemoryMap<Key, Value> extends NitriteMap<Key, Value> {
final SplayTreeMap<Key, Value> _backingMap;
final NitriteStore _nitriteStore;
final String _mapName;
bool _droppedFlag = false;
bool _closedFlag = false;
InMemoryMap(this._mapName, this._nitriteStore)
: _backingMap = SplayTreeMap<Key, Value>(_comp);
@override
String get name => _mapName;
@override
Future<bool> containsKey(Key key) async {
_checkOpened();
return _backingMap.containsKey(key);
}
@override
Future<Value?> operator [](Key key) async {
_checkOpened();
return _backingMap[key];
}
@override
NitriteStore<Config> getStore<Config extends StoreConfig>() {
return _nitriteStore as NitriteStore<Config>;
}
@override
Stream<Value> values() {
_checkOpened();
return Stream.fromIterable(_backingMap.values);
}
@override
Future<Value?> remove(Key key) async {
_checkOpened();
var val = _backingMap.remove(key);
await updateLastModifiedTime();
return val;
}
@override
Stream<Key> keys() {
_checkOpened();
return Stream.fromIterable(_backingMap.keys);
}
@override
Future<void> put(Key key, Value value) {
_checkOpened();
_backingMap[key] = value;
return updateLastModifiedTime();
}
@override
Future<int> size() async {
_checkOpened();
return _backingMap.length;
}
@override
Future<Value?> putIfAbsent(Key key, Value value) async {
_checkOpened();
if (await containsKey(key)) {
return _backingMap[key];
} else {
_backingMap[key] = value;
await updateLastModifiedTime();
return null;
}
}
@override
Stream<(Key, Value)> entries() {
_checkOpened();
return Stream.fromIterable(
_backingMap.entries.map((e) => (e.key, e.value)));
}
@override
Stream<(Key, Value)> reversedEntries() {
_checkOpened();
return Stream.fromIterable(
_backingMap.reversedEntries.map((e) => (e.key, e.value)));
}
@override
Future<Key?> higherKey(Key key) async {
_checkOpened();
if (key == null) return null;
return _backingMap.higherKey(key);
}
@override
Future<Key?> ceilingKey(Key key) async {
_checkOpened();
if (key == null) return null;
return _backingMap.ceilingKey(key);
}
@override
Future<Key?> lowerKey(Key key) async {
_checkOpened();
if (key == null) return null;
return _backingMap.lowerKey(key);
}
@override
Future<Key?> floorKey(Key key) async {
_checkOpened();
if (key == null) return null;
return _backingMap.floorKey(key);
}
@override
Future<bool> isEmpty() async {
_checkOpened();
return _backingMap.isEmpty;
}
@override
Future<void> drop() async {
if (!_droppedFlag) {
_backingMap.clear();
await getStore().removeMap(_mapName);
_droppedFlag = true;
_closedFlag = true;
}
}
@override
bool get isDropped => _droppedFlag;
@override
Future<void> close() async {
_closedFlag = true;
await _nitriteStore.closeMap(_mapName);
}
@override
bool get isClosed => _closedFlag;
@override
Future<void> clear() async {
_checkOpened();
_backingMap.clear();
await _nitriteStore.closeMap(_mapName);
await updateLastModifiedTime();
}
static int _comp(k1, k2) {
if (k1 is Comparable && k2 is Comparable) {
return compare(k1, k2);
}
return Comparable.compare(k1, k2);
}
void _checkOpened() {
if (_closedFlag) {
throw InvalidOperationException('Map $_mapName is closed');
}
if (_droppedFlag) {
throw InvalidOperationException('Map $_mapName is dropped');
}
}
@override
Future<void> initialize() async {}
}
#
Nitrite RTree
The following code snippet shows the implementation of the NitriteRTree
interface.
class InMemoryRTree<Key extends BoundingBox, Value>
extends NitriteRTree<Key, Value> {
final Map<SpatialKey, Key?> _backingMap = <SpatialKey, Key?>{};
final NitriteStore _nitriteStore;
final String _mapName;
bool _droppedFlag = false;
bool _closedFlag = false;
InMemoryRTree(this._mapName, this._nitriteStore);
@override
Future<int> size() async {
_checkOpened();
return _backingMap.length;
}
@override
Future<void> add(Key? key, NitriteId? value) async {
_checkOpened();
if (value != null) {
var spatialKey = _getKey(key, int.parse(value.idValue));
_backingMap[spatialKey] = key;
}
}
@override
Future<void> remove(Key? key, NitriteId? value) async {
_checkOpened();
if (value != null) {
var spatialKey = _getKey(key, int.parse(value.idValue));
_backingMap.remove(spatialKey);
}
}
@override
Stream<NitriteId> findIntersectingKeys(Key? key) async* {
_checkOpened();
var spatialKey = _getKey(key, 0);
for (var sk in _backingMap.keys) {
if (_isOverlap(sk, spatialKey)) {
yield NitriteId.createId(sk.id.toString());
}
}
}
@override
Stream<NitriteId> findContainedKeys(Key? key) async* {
_checkOpened();
var spatialKey = _getKey(key, 0);
for (var sk in _backingMap.keys) {
if (_isInside(sk, spatialKey)) {
yield NitriteId.createId(sk.id.toString());
}
}
}
@override
Future<void> clear() async {
_checkOpened();
_backingMap.clear();
await _nitriteStore.closeRTree(_mapName);
}
@override
Future<void> close() async {
_closedFlag = true;
await _nitriteStore.closeRTree(_mapName);
}
@override
Future<void> drop() async {
_checkOpened();
_droppedFlag = true;
_backingMap.clear();
await _nitriteStore.removeRTree(_mapName);
}
SpatialKey _getKey(Key? key, int id) {
if (key == null || key == BoundingBox.empty) {
return SpatialKey(id, []);
}
return SpatialKey(id, [key.minX, key.maxX, key.minY, key.maxY]);
}
bool _isOverlap(SpatialKey a, SpatialKey b) {
if (a.isNull() || b.isNull()) {
return false;
}
for (var i = 0; i < 2; i++) {
if (a.max(i) < b.min(i) || a.min(i) > b.max(i)) {
return false;
}
}
return true;
}
bool _isInside(SpatialKey a, SpatialKey b) {
if (a.isNull() || b.isNull()) {
return false;
}
for (var i = 0; i < 2; i++) {
if (a.min(i) <= b.min(i) || a.max(i) >= b.max(i)) {
return false;
}
}
return true;
}
void _checkOpened() {
if (_closedFlag) {
throw NitriteException('RTreeMap is closed');
}
if (_droppedFlag) {
throw NitriteException('RTreeMap is dropped');
}
}
@override
Future<void> initialize() async {}
}