Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented backing store abstractions #10

Merged
merged 9 commits into from
Mar 19, 2024
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
Loading