Skip to content

Commit

Permalink
Merge pull request #10 from ricardoboss/issues/6-backing-store
Browse files Browse the repository at this point in the history
Implemented backing store abstractions
  • Loading branch information
ricardoboss authored Mar 19, 2024
2 parents 9c459ed + 8f159e8 commit a6a5c60
Show file tree
Hide file tree
Showing 12 changed files with 495 additions and 0 deletions.
9 changes: 9 additions & 0 deletions lib/kiota_abstractions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ part 'src/serialization/serialization_writer_factory.dart';
part 'src/serialization/serialization_writer_factory_registry.dart';
part 'src/time_only.dart';
part 'src/serialization/serialization_writer_proxy_factory.dart';
part 'src/store/backed_model.dart';
part 'src/store/backing_store.dart';
part 'src/store/backing_store_factory.dart';
part 'src/store/backing_store_factory_singleton.dart';
part 'src/store/backing_store_parse_node_factory.dart';
part 'src/store/backing_store_serialization_writer_proxy_factory.dart';
part 'src/store/backing_store_subscription_callback.dart';
part 'src/store/in_memory_backing_store.dart';
part 'src/store/in_memory_backing_store_factory.dart';
7 changes: 7 additions & 0 deletions lib/src/store/backed_model.dart
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;
}
46 changes: 46 additions & 0 deletions lib/src/store/backing_store.dart
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;
}
7 changes: 7 additions & 0 deletions lib/src/store/backing_store_factory.dart
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();
}
9 changes: 9 additions & 0 deletions lib/src/store/backing_store_factory_singleton.dart
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;
}
28 changes: 28 additions & 0 deletions lib/src/store/backing_store_parse_node_factory.dart
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;
}
}
},
);
}
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);
});
}
}
},
);
}
9 changes: 9 additions & 0 deletions lib/src/store/backing_store_subscription_callback.dart
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,
);
175 changes: 175 additions & 0 deletions lib/src/store/in_memory_backing_store.dart
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);
}
}
7 changes: 7 additions & 0 deletions lib/src/store/in_memory_backing_store_factory.dart
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();
}
Loading

0 comments on commit a6a5c60

Please sign in to comment.