-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from ricardoboss/issues/6-backing-store
Implemented backing store abstractions
- Loading branch information
Showing
12 changed files
with
495 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// Defines the contract for a model that is backed by a store. | ||
abstract class BackedModel { | ||
/// Gets the store that is backing the model. | ||
BackingStore? get backingStore; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// Stores model information in a different location than the object properties. | ||
/// | ||
/// Implementations can provide dirty tracking and caching capabilities or | ||
/// integration with 3rd party stores. | ||
abstract class BackingStore { | ||
/// Gets a value from the backing store based on its key. Returns `null` if | ||
/// the value hasn't changed and [returnOnlyChangedValues] is `true`. | ||
T? get<T>(String key); | ||
|
||
/// Sets or updates the stored value for the given key. | ||
/// | ||
/// Will trigger subscriptions callbacks. | ||
void set<T>(String key, T value); | ||
|
||
/// Iterates all the values stored in the backing store. Values will be | ||
/// filtered if [returnOnlyChangedValues] is `true`. | ||
Iterable<MapEntry<String, Object?>> iterate(); | ||
|
||
/// Iterates the keys for all values that changed to `null`. | ||
Iterable<String> iterateKeysForValuesChangedToNull(); | ||
|
||
/// Creates a subscription to any data change happening, optionally specifying | ||
/// a [subscriptionId] to be able to unsubscribe later. | ||
/// | ||
/// The given [callback] is invoked on data changes. | ||
String subscribe( | ||
BackingStoreSubscriptionCallback callback, [ | ||
String? subscriptionId, | ||
]); | ||
|
||
/// Unsubscribes a subscription by its [subscriptionId]. | ||
void unsubscribe(String subscriptionId); | ||
|
||
/// Clears all the stored values. Doesn't trigger any subscription callbacks. | ||
void clear(); | ||
|
||
/// Whether to return only values that have changed since the initialization | ||
/// of the object when calling [get] and [iterate] methods. | ||
abstract bool returnOnlyChangedValues; | ||
|
||
/// Whether the initialization of the object and/or the initial | ||
/// deserialization has been competed to track whether objects have changed. | ||
abstract bool initializationCompleted; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// Defines the contract for a factory that creates a [BackingStore]. | ||
abstract class BackingStoreFactory { | ||
/// Creates a new instance of the [BackingStore]. | ||
BackingStore createBackingStore(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// This class is used to register the backing store factory. | ||
class BackingStoreFactorySingleton { | ||
static final BackingStoreFactory _instance = InMemoryBackingStoreFactory(); | ||
|
||
/// The backing store factory singleton instance. | ||
static BackingStoreFactory get instance => _instance; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// Proxy implementation of [ParseNodeFactory] that allows for the | ||
/// [BackingStore] that automatically sets the state of the [BackingStore] | ||
/// when deserializing. | ||
class BackingStoreParseNodeFactory extends ParseNodeProxyFactory { | ||
/// Creates a new instance of the [BackingStoreParseNodeFactory] class. | ||
BackingStoreParseNodeFactory({ | ||
required super.concrete, | ||
}) : super( | ||
onBefore: (parsable) { | ||
if (parsable is BackedModel) { | ||
final model = parsable as BackedModel; | ||
if (model.backingStore != null) { | ||
model.backingStore!.initializationCompleted = false; | ||
} | ||
} | ||
}, | ||
onAfter: (parsable) { | ||
if (parsable is BackedModel) { | ||
final model = parsable as BackedModel; | ||
if (model.backingStore != null) { | ||
model.backingStore!.initializationCompleted = true; | ||
} | ||
} | ||
}, | ||
); | ||
} |
42 changes: 42 additions & 0 deletions
42
lib/src/store/backing_store_serialization_writer_proxy_factory.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// Proxy implementation of [SerializationWriterFactory] for the [BackingStore] | ||
/// that automatically sets the state of the backing store when serializing. | ||
class BackingStoreSerializationWriterProxyFactory | ||
extends SerializationWriterProxyFactory { | ||
/// Creates a new instance of [BackingStoreSerializationWriterProxyFactory] | ||
/// with the provided concrete factory. | ||
BackingStoreSerializationWriterProxyFactory({ | ||
required super.concrete, | ||
}) : super( | ||
onBefore: (p) { | ||
if (p is BackedModel) { | ||
final model = p as BackedModel; | ||
if (model.backingStore != null) { | ||
model.backingStore!.returnOnlyChangedValues = true; | ||
} | ||
} | ||
}, | ||
onAfter: (p) { | ||
if (p is BackedModel) { | ||
final model = p as BackedModel; | ||
if (model.backingStore != null) { | ||
model.backingStore!.returnOnlyChangedValues = false; | ||
model.backingStore!.initializationCompleted = true; | ||
} | ||
} | ||
}, | ||
onStart: (p, writer) { | ||
if (p is BackedModel) { | ||
final model = p as BackedModel; | ||
if (model.backingStore != null) { | ||
model.backingStore! | ||
.iterateKeysForValuesChangedToNull() | ||
.forEach((element) { | ||
writer.writeNullValue(element); | ||
}); | ||
} | ||
} | ||
}, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// Defines the contract for a callback that is invoked when a value in the | ||
/// [BackingStore] changes. | ||
typedef BackingStoreSubscriptionCallback = void Function( | ||
String dataKey, | ||
Object? previousValue, | ||
Object? newValue, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
class InMemoryBackingStore implements BackingStore { | ||
final Map<String, (bool, Object?)> _store = {}; | ||
final Map<String, BackingStoreSubscriptionCallback> _subscriptions = {}; | ||
|
||
bool _initializationCompleted = true; | ||
|
||
@override | ||
bool get initializationCompleted => _initializationCompleted; | ||
|
||
@override | ||
set initializationCompleted(bool value) { | ||
_initializationCompleted = value; | ||
|
||
for (final key in _store.keys) { | ||
final tuple = _store[key]!; | ||
final obj = tuple.$2; | ||
|
||
if (obj is BackedModel) { | ||
obj.backingStore?.initializationCompleted = value; | ||
} | ||
|
||
_ensureCollectionPropertyIsConsistent(key, obj); | ||
|
||
_store[key] = (!value, obj); | ||
} | ||
} | ||
|
||
void _ensureCollectionPropertyIsConsistent(String key, Object? value) { | ||
// check if we put in a collection annotated with the size | ||
if (value is (Iterable<Object?>, int)) { | ||
value.$1.whereType<BackedModel>().forEach((model) { | ||
model.backingStore?.iterate().forEach((item) { | ||
// Call get() on nested properties so that this method may be called | ||
// recursively to ensure collections are consistent | ||
model.backingStore?.get<Object?>(item.key); | ||
}); | ||
}); | ||
|
||
// (and the size has changed since we last updated) | ||
if (value.$1.length != value.$2) { | ||
// ensure the store is notified the collection property has changed | ||
set(key, value.$1); | ||
} | ||
} else if (value is BackedModel) { | ||
value.backingStore?.iterate().forEach((item) { | ||
// Call get() on nested properties so that this method may be called | ||
// recursively to ensure collections are consistent | ||
value.backingStore?.get<Object?>(item.key); | ||
}); | ||
} | ||
} | ||
|
||
@override | ||
bool returnOnlyChangedValues = false; | ||
|
||
@override | ||
void clear() => _store.clear(); | ||
|
||
@override | ||
T? get<T>(String key) { | ||
if (key.isEmpty) { | ||
throw ArgumentError('The key cannot be empty.'); | ||
} | ||
|
||
if (!_store.containsKey(key)) { | ||
return null; | ||
} | ||
|
||
final tuple = _store[key]!; | ||
final changed = tuple.$1; | ||
var obj = tuple.$2; | ||
|
||
_ensureCollectionPropertyIsConsistent(key, obj); | ||
|
||
if (obj is (Iterable<Object?>, int)) { | ||
obj = obj.$1; | ||
} | ||
|
||
return changed || !returnOnlyChangedValues ? obj as T? : null; | ||
} | ||
|
||
@override | ||
Iterable<MapEntry<String, Object?>> iterate() sync* { | ||
for (final key in _store.keys) { | ||
final tuple = _store[key]!; | ||
final changed = tuple.$1; | ||
final obj = tuple.$2; | ||
|
||
if (returnOnlyChangedValues) { | ||
_ensureCollectionPropertyIsConsistent(key, obj); | ||
} | ||
|
||
if (changed || !returnOnlyChangedValues) { | ||
yield MapEntry(key, obj); | ||
} | ||
} | ||
} | ||
|
||
@override | ||
Iterable<String> iterateKeysForValuesChangedToNull() sync* { | ||
for (final key in _store.keys) { | ||
final tuple = _store[key]!; | ||
final changed = tuple.$1; | ||
final obj = tuple.$2; | ||
|
||
if (changed && obj == null) { | ||
yield key; | ||
} | ||
} | ||
} | ||
|
||
@override | ||
void set<T>(String key, T value) { | ||
if (key.isEmpty) { | ||
throw ArgumentError('The key cannot be empty.'); | ||
} | ||
|
||
(bool, Object?) valueToAdd = (initializationCompleted, value); | ||
if (value is Iterable<Object?>) { | ||
valueToAdd = (initializationCompleted, (value, value.length)); | ||
} | ||
|
||
(bool, Object?)? oldValue; | ||
if (_store.containsKey(key)) { | ||
oldValue = _store[key]; | ||
} else if (value is BackedModel) { | ||
value.backingStore?.subscribe( | ||
(dataKey, previousValue, newValue) { | ||
// all its properties are dirty as the model has been touched | ||
value.backingStore!.initializationCompleted = false; | ||
|
||
set(key, value); | ||
}, | ||
key, | ||
); | ||
} | ||
|
||
_store[key] = valueToAdd; | ||
|
||
if (value is Iterable<Object?>) { | ||
value.whereType<BackedModel>().forEach((model) { | ||
model.backingStore?.initializationCompleted = false; | ||
model.backingStore?.subscribe( | ||
(dataKey, previousValue, newValue) { | ||
set(key, value); | ||
}, | ||
key, | ||
); | ||
}); | ||
} | ||
|
||
for (final subscription in _subscriptions.values) { | ||
subscription(key, oldValue?.$2, value); | ||
} | ||
} | ||
|
||
@override | ||
String subscribe( | ||
BackingStoreSubscriptionCallback callback, [ | ||
String? subscriptionId, | ||
]) { | ||
subscriptionId ??= const Uuid().v4(); | ||
|
||
_subscriptions[subscriptionId] = callback; | ||
|
||
return subscriptionId; | ||
} | ||
|
||
@override | ||
void unsubscribe(String subscriptionId) { | ||
_subscriptions.remove(subscriptionId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
part of '../../kiota_abstractions.dart'; | ||
|
||
/// This class is used to create instances of [InMemoryBackingStore]. | ||
class InMemoryBackingStoreFactory implements BackingStoreFactory { | ||
@override | ||
BackingStore createBackingStore() => InMemoryBackingStore(); | ||
} |
Oops, something went wrong.