Skip to content

Commit

Permalink
Enhance subscriptions to not emit updates when the underlying databas…
Browse files Browse the repository at this point in the history
…e value has not changed

Fix unsubscribe (dispose) for queries
  • Loading branch information
tp committed Oct 23, 2024
1 parent e8d03b0 commit 919e97d
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 30 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.4.0

* Enhance subscriptions to not emit updates when the underlying database value has not changed
* E.g. when an object is updated with the exact same storage values or a query ends up with the same result list no updates are emitted
* Fix unsubscribe for queries

## 1.3.0

* Add `single`/`singleOnce` for cases where one expects a single match, but does not have a primary key
Expand Down
111 changes: 86 additions & 25 deletions lib/src/index_entity_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ part 'index_columns.dart';
part 'query.dart';
part 'query_result.dart';

typedef QueryResultMapping<T> = (T Function(), QueryResult<T>);
typedef QueryResultMapping<T> = (MappedDBResult<T> Function(), QueryResult<T>);

enum SortOrder {
asc,
Expand Down Expand Up @@ -45,16 +45,17 @@ class IndexedEntityStore<T, K> {
final List<QueryResultMapping> _entityResults = [];

@visibleForTesting
int get subscriptionCount =>
_singleEntityResults.values.expand((mappings) => mappings).length +
_entityResults.length;
int get subscriptionCount {
return _singleEntityResults.values.expand((mappings) => mappings).length +
_entityResults.length;
}

/// Returns a subscription to a single entity by its primary key
QueryResult<T?> get(K key) {
final QueryResultMapping<T?> mapping = (
() => getOnce(key),
() => _getOnce(key),
QueryResult._(
initialValue: getOnce(key),
initialValue: _getOnce(key),
onDispose: (r) {
_singleEntityResults[key] = _singleEntityResults[key]!
.where((mapping) => mapping.$2 != r)
Expand All @@ -74,28 +75,34 @@ class IndexedEntityStore<T, K> {

/// Returns a single entity by its primary key
T? getOnce(K key) {
return _getOnce(key).result;
}

MappedDBResult<T?> _getOnce(K key) {
final res = _database.select(
'SELECT value FROM `entity` WHERE `type` = ? AND `key` = ?',
[_entityKey, key],
);

if (res.isEmpty) {
return null;
return (dbValues: [null], result: null);
}

return _connector.deserialize(res.single['value']);
final dbValue = res.single['value'];

return (dbValues: [dbValue], result: _connector.deserialize(dbValue));
}

/// Returns a subscription to all entities in this store
QueryResult<List<T>> getAll({
OrderByClause? orderBy,
}) {
final QueryResultMapping<List<T>> mapping = (
() => getAllOnce(orderBy: orderBy),
() => _getAllOnce(orderBy: orderBy),
QueryResult._(
initialValue: getAllOnce(orderBy: orderBy),
initialValue: _getAllOnce(orderBy: orderBy),
onDispose: (r) {
_entityResults.removeWhere((m) => m.$2 != r);
_entityResults.removeWhere((m) => m.$2 == r);
},
)
);
Expand All @@ -108,6 +115,12 @@ class IndexedEntityStore<T, K> {
/// Returns a list of all entities in this store
List<T> getAllOnce({
OrderByClause? orderBy,
}) {
return _getAllOnce(orderBy: orderBy).result;
}

MappedDBResult<List<T>> _getAllOnce({
OrderByClause? orderBy,
}) {
final res = _database.select(
[
Expand All @@ -124,7 +137,12 @@ class IndexedEntityStore<T, K> {
],
);

return res.map((e) => _connector.deserialize(e['value'])).toList();
final values = res.map((e) => e['value']).toList();

return (
dbValues: values,
result: values.map((v) => _connector.deserialize(v)).toList()
);
}

/// Returns the single entity (or null) for the given query
Expand All @@ -134,11 +152,11 @@ class IndexedEntityStore<T, K> {
/// they can use [query] with a limit instead.
QueryResult<T?> single(QueryBuilder q) {
final QueryResultMapping<T?> mapping = (
() => singleOnce(q),
() => _singleOnce(q),
QueryResult._(
initialValue: singleOnce(q),
initialValue: _singleOnce(q),
onDispose: (r) {
_entityResults.removeWhere((m) => m.$2 != r);
_entityResults.removeWhere((m) => m.$2 == r);
},
)
);
Expand All @@ -154,14 +172,19 @@ class IndexedEntityStore<T, K> {
/// If the caller expects more than 1 value but is only interested in one,
/// they can use [query] with a limit instead.
T? singleOnce(QueryBuilder q) {
final result = queryOnce(q, limit: 2);
return _singleOnce(q).result;
}

MappedDBResult<T?> _singleOnce(QueryBuilder q) {
final result = _queryOnce(q, limit: 2);

if (result.length > 1) {
if (result.result.length > 1) {
throw Exception(
'singleOnce expected to find one element, but found at least 2 matching the query $q');
'singleOnce expected to find one element, but found at least 2 matching the query $q',
);
}

return result.singleOrNull;
return (dbValues: result.dbValues, result: result.result.firstOrNull);
}

/// Returns a subscription to entities matching the given query
Expand All @@ -171,11 +194,11 @@ class IndexedEntityStore<T, K> {
int? limit,
}) {
final QueryResultMapping<List<T>> mapping = (
() => queryOnce(q, limit: limit, orderBy: orderBy),
() => _queryOnce(q, limit: limit, orderBy: orderBy),
QueryResult._(
initialValue: queryOnce(q, limit: limit, orderBy: orderBy),
initialValue: _queryOnce(q, limit: limit, orderBy: orderBy),
onDispose: (r) {
_entityResults.removeWhere((m) => m.$2 != r);
_entityResults.removeWhere((m) => m.$2 == r);
},
)
);
Expand All @@ -190,6 +213,14 @@ class IndexedEntityStore<T, K> {
QueryBuilder q, {
OrderByClause? orderBy,
int? limit,
}) {
return _queryOnce(q, orderBy: orderBy, limit: limit).result;
}

MappedDBResult<List<T>> _queryOnce(
QueryBuilder q, {
OrderByClause? orderBy,
int? limit,
}) {
final (w, s) = q(_indexColumns)._entityKeysQuery();

Expand All @@ -211,7 +242,12 @@ class IndexedEntityStore<T, K> {

final res = _database.select(query, values);

return res.map((e) => _connector.deserialize(e['value'])).toList();
final dbValues = res.map((e) => e['value']).toList();

return (
dbValues: dbValues,
result: dbValues.map((v) => _connector.deserialize(v)).toList(),
);
}

void insert(T e) {
Expand Down Expand Up @@ -257,6 +293,14 @@ class IndexedEntityStore<T, K> {
delete(_connector.getPrimaryKey(entity));
}

void deleteEntities(Iterable<T> entities) {
deleteMany(
{
for (final e in entities) _connector.getPrimaryKey(e),
},
);
}

void deleteMany(Set<K> keys) {
for (final key in keys) {
_database.execute(
Expand Down Expand Up @@ -307,13 +351,30 @@ class IndexedEntityStore<T, K> {
final singleEntitySubscriptions = _singleEntityResults[key];
if (singleEntitySubscriptions != null) {
for (final mapping in singleEntitySubscriptions) {
mapping.$2._value.value = mapping.$1();
final newValue = mapping.$1();

if (newValue.dbValues.length ==
mapping.$2._value.value.dbValues.length &&
newValue.dbValues.indexed.every(
(e) => mapping.$2._value.value.dbValues[e.$1] == e.$2)) {
continue; // values already match
}

mapping.$2._value.value = newValue;
}
}
}

for (final mapping in _entityResults) {
mapping.$2._value.value = mapping.$1();
final newValue = mapping.$1();

if (newValue.dbValues.length == mapping.$2._value.value.dbValues.length &&
newValue.dbValues.indexed
.every((e) => mapping.$2._value.value.dbValues[e.$1] == e.$2)) {
continue; // values already match
}

mapping.$2._value.value = newValue;
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions lib/src/query_result.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
part of 'index_entity_store.dart';

typedef MappedDBResult<T> = ({List<dynamic> dbValues, T result});

class QueryResult<T> implements ValueListenable<T> {
QueryResult._({
required T initialValue,
required MappedDBResult<T> initialValue,
void Function(QueryResult<T> self)? onDispose,
}) : _value = ValueNotifier(initialValue),
_onDispose = onDispose;

final ValueNotifier<T> _value;
final ValueNotifier<MappedDBResult<T>> _value;

final void Function(QueryResult<T> self)? _onDispose;

Expand All @@ -22,7 +24,7 @@ class QueryResult<T> implements ValueListenable<T> {
}

@override
T get value => _value.value;
T get value => _value.value.result;

@mustCallSuper
void dispose() {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: indexed_entity_store
description: A fast, simple, and synchronous entity store for Flutter applications.
version: 1.3.0
version: 1.4.0
repository: https://github.com/LunaONE/indexed_entity_store

environment:
Expand Down
Loading

0 comments on commit 919e97d

Please sign in to comment.