-
Notifications
You must be signed in to change notification settings - Fork 26
Track collapsable client events, that are handled optimistically (#444) #768
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
base: main
Are you sure you want to change the base?
Changes from all commits
f279e77
1156cd5
5fba0b3
1a31ada
35b2b8b
1829c57
4759c8f
992db73
e4ee0f8
202f625
b631bdd
9737776
e0a10fe
170642a
e719bc1
1c698c7
f4dcc2b
0d6d27a
7a3d14b
06cb530
8b416d3
3b61918
a5251b3
7dcfba4
8670698
782d446
4ab72c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ import '/store/pagination.dart'; | |
import '/store/pagination/hive.dart'; | ||
import '/store/pagination/hive_graphql.dart'; | ||
import '/ui/page/home/page/chat/controller.dart' show ChatViewExt; | ||
import '/util/event_pool.dart'; | ||
import '/util/log.dart'; | ||
import '/util/new_type.dart'; | ||
import '/util/obs/obs.dart'; | ||
|
@@ -1158,6 +1159,10 @@ class HiveRxChat extends RxChat { | |
if (!subscribed) { | ||
return; | ||
} | ||
if (_chatRepository.eventPool.ignore(event.toPoolEntry())) { | ||
shouldPutChat = false; | ||
continue; | ||
} | ||
Comment on lines
+1162
to
+1165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вообще эту штуку в теории можно перенести в |
||
|
||
switch (event.kind) { | ||
case ChatEventKind.redialed: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ import '/domain/repository/user.dart'; | |
import '/provider/gql/exceptions.dart'; | ||
import '/provider/gql/graphql.dart'; | ||
import '/provider/hive/my_user.dart'; | ||
import '/util/event_pool.dart'; | ||
import '/util/log.dart'; | ||
import '/util/new_type.dart'; | ||
import '/util/platform_utils.dart'; | ||
|
@@ -69,6 +70,9 @@ class MyUserRepository implements AbstractMyUserRepository { | |
/// GraphQL API provider. | ||
final GraphQlProvider _graphQlProvider; | ||
|
||
/// [EventPool] of this [MyUserRepository]. | ||
final EventPool eventPool = EventPool(); | ||
|
||
/// [MyUser] local [Hive] storage. | ||
final MyUserHiveProvider _myUserLocal; | ||
|
||
|
@@ -490,12 +494,22 @@ class MyUserRepository implements AbstractMyUserRepository { | |
|
||
myUser.update((u) => u?.muted = muting?.toModel()); | ||
|
||
try { | ||
await _graphQlProvider.toggleMyUserMute(muting); | ||
} catch (e) { | ||
myUser.update((u) => u?.muted = muted); | ||
rethrow; | ||
} | ||
await eventPool.add(PoolEntry( | ||
type: EventType.myUserMuteChatsToggled, | ||
key: EventType.myUserMuteChatsToggled.hashCode, | ||
propsHash: Object.hash( | ||
EventType.myUserMuteChatsToggled, | ||
(muting != null), | ||
), | ||
handler: () async { | ||
try { | ||
await _graphQlProvider.toggleMyUserMute(muting); | ||
} catch (e) { | ||
myUser.update((u) => u?.muted = muted); | ||
rethrow; | ||
} | ||
}, | ||
)); | ||
Comment on lines
+497
to
+512
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В отличие от |
||
} | ||
|
||
@override | ||
|
@@ -680,6 +694,10 @@ class MyUserRepository implements AbstractMyUserRepository { | |
} | ||
} | ||
|
||
if (eventPool.ignore(event.toPoolEntry())) { | ||
return; | ||
} | ||
|
||
switch (event.kind) { | ||
case MyUserEventKind.nameUpdated: | ||
event as EventUserNameUpdated; | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,184 @@ | ||||||
// Copyright © 2022-2023 IT ENGINEERING MANAGEMENT INC, | ||||||
// <https://github.com/team113> | ||||||
// | ||||||
// This program is free software: you can redistribute it and/or modify it under | ||||||
// the terms of the GNU Affero General Public License v3.0 as published by the | ||||||
// Free Software Foundation, either version 3 of the License, or (at your | ||||||
// option) any later version. | ||||||
// | ||||||
// This program is distributed in the hope that it will be useful, but WITHOUT | ||||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License v3.0 for | ||||||
// more details. | ||||||
// | ||||||
// You should have received a copy of the GNU Affero General Public License v3.0 | ||||||
// along with this program. If not, see | ||||||
// <https://www.gnu.org/licenses/agpl-3.0.html>. | ||||||
|
||||||
import 'package:get/get.dart'; | ||||||
import 'package:mutex/mutex.dart'; | ||||||
|
||||||
import '/store/event/my_user.dart'; | ||||||
import '/store/event/chat.dart'; | ||||||
import 'log.dart'; | ||||||
|
||||||
/// Tracker for optimistic events. | ||||||
class EventPool { | ||||||
/// Adds [PoolEntry] to this [EventPool] and awaits for handling. | ||||||
Future<void> add(PoolEntry? event) async { | ||||||
Log.debug('add($event)', '$runtimeType'); | ||||||
|
||||||
if (event != null) { | ||||||
if (_collapses[event.key] != null) { | ||||||
if (!(_collapses[event.key] == event)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
? |
||||||
// Collapse with registred [PoolEntry] | ||||||
10000
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
_collapses.remove(event.key); | ||||||
} | ||||||
} else { | ||||||
_collapses[event.key] = event; | ||||||
|
||||||
_locks[event.key] ??= Mutex(); | ||||||
await _locks[event.key]?.protect(() async { | ||||||
await _collapsableWrapper(event); | ||||||
}); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/// Indicates whether [PoolEntry] should be ignored. | ||||||
bool ignore(PoolEntry? event) { | ||||||
Log.debug('ignore($event)', '$runtimeType'); | ||||||
|
||||||
if (event != null) { | ||||||
final PoolEntry? waiter = | ||||||
_ignores[event.key]?.firstWhereOrNull((element) => event == element); | ||||||
if (waiter != null) { | ||||||
_ignores[event.key]!.remove(waiter); | ||||||
return true; | ||||||
} | ||||||
} | ||||||
return false; | ||||||
} | ||||||
|
||||||
/// [Mutex]es guarding synchronized call [PoolEntry] handlers. | ||||||
final Map<int, Mutex> _locks = {}; | ||||||
Comment on lines
+49
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
/// [PoolEntry]es, that should be ignored when resieved from back. | ||||||
final Map<int, List<PoolEntry>> _ignores = {}; | ||||||
|
||||||
/// [PoolEntry]es, that should be collapsed with event of same type. | ||||||
final Map<int, PoolEntry> _collapses = {}; | ||||||
|
||||||
/// Implements collapsable behavior for handle [PoolEntry]. | ||||||
Future<dynamic> _collapsableWrapper(PoolEntry event) async { | ||||||
if (_collapses[event.key] != event) { | ||||||
// This event was collapsed. | ||||||
return; | ||||||
} else { | ||||||
_collapses.remove(event.key); | ||||||
} | ||||||
|
||||||
_ignores[event.key] ??= []; | ||||||
_ignores[event.key]!.add(event); | ||||||
|
||||||
await event.handler?.call(); | ||||||
} | ||||||
} | ||||||
|
||||||
/// Entry for tracking events in [EventPool]. | ||||||
class PoolEntry { | ||||||
PoolEntry( | ||||||
{required this.key, | ||||||
required this.propsHash, | ||||||
required this.type, | ||||||
this.handler}); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
https://dart.dev/effective-dart/style#consider-changing-your-code-to-make-it-more-formatter-friendly |
||||||
|
||||||
/// Handler of this [PoolEntry]. | ||||||
/// | ||||||
/// It would be called when handlers of other [PoolEntry] with same [key] | ||||||
/// would be complited. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Поставьте spell checker в Ваш IDE. |
||||||
final Future<void> Function()? handler; | ||||||
|
||||||
/// [EventType] of this [PoolEntry]. | ||||||
final EventType type; | ||||||
|
||||||
/// Key of this [PoolEntry]. | ||||||
final int key; | ||||||
|
||||||
/// Stores hashed valuable properties of event, realted to this [PoolEntry]. | ||||||
final int propsHash; | ||||||
|
||||||
@override | ||||||
int get hashCode => propsHash; | ||||||
|
||||||
@override | ||||||
bool operator ==(Object other) => | ||||||
other is PoolEntry && propsHash == other.propsHash; | ||||||
Comment on lines
+111
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Я не уверен, что за сравнения такого рода должен отвечать этот |
||||||
|
||||||
@override | ||||||
String toString() => '$runtimeType($type, $key, $propsHash)'; | ||||||
} | ||||||
|
||||||
/// Types of tracking events. | ||||||
enum EventType { | ||||||
myUserMuteChatsToggled, | ||||||
chatFavoriteToggled; | ||||||
} | ||||||
Comment on lines
+123
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Почему утилита знает о специфике стора? |
||||||
|
||||||
/// Extension adding [PoolEntry] construction from a [MyUserEvent]. | ||||||
extension MyUserEventToPoolEntryExtension on MyUserEvent { | ||||||
/// Constructs a new [PoolEntry] from this [MyUserEvent]. | ||||||
PoolEntry? toPoolEntry([Future<void> Function()? handler]) { | ||||||
return switch (kind) { | ||||||
MyUserEventKind.userMuted => PoolEntry( | ||||||
type: EventType.myUserMuteChatsToggled, | ||||||
key: EventType.myUserMuteChatsToggled.hashCode, | ||||||
propsHash: Object.hash( | ||||||
EventType.myUserMuteChatsToggled, | ||||||
true, | ||||||
), | ||||||
handler: handler, | ||||||
), | ||||||
MyUserEventKind.unmuted => PoolEntry( | ||||||
type: EventType.myUserMuteChatsToggled, | ||||||
key: EventType.myUserMuteChatsToggled.hashCode, | ||||||
propsHash: Object.hash( | ||||||
EventType.myUserMuteChatsToggled, | ||||||
false, | ||||||
), | ||||||
handler: handler, | ||||||
), | ||||||
_ => null, | ||||||
}; | ||||||
} | ||||||
} | ||||||
|
||||||
/// Extension adding [PoolEntry] construction from a [ChatEvent]. | ||||||
extension ChatEventToPoolEntryExtension on ChatEvent { | ||||||
/// Constructs a new [PoolEntry] from this [ChatEvent]. | ||||||
PoolEntry? toPoolEntry([Future<void> Function()? handler]) { | ||||||
return switch (kind) { | ||||||
ChatEventKind.favorited => PoolEntry( | ||||||
type: EventType.chatFavoriteToggled, | ||||||
Comment on lines
+159
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Не уверен вовсе, что есть смысл в создании неких |
||||||
key: Object.hash(EventType.chatFavoriteToggled, chatId), | ||||||
propsHash: Object.hash( | ||||||
EventType.chatFavoriteToggled, | ||||||
chatId, | ||||||
true, | ||||||
), | ||||||
handler: handler, | ||||||
), | ||||||
ChatEventKind.unfavorited => PoolEntry( | ||||||
type: EventType.chatFavoriteToggled, | ||||||
key: Object.hash(EventType.chatFavoriteToggled, chatId), | ||||||
propsHash: Object.hash( | ||||||
EventType.chatFavoriteToggled, | ||||||
chatId, | ||||||
false, | ||||||
), | ||||||
handler: handler, | ||||||
), | ||||||
_ => null, | ||||||
}; | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А почему это в домене лежит? Разве это часть бизнес-логики? Нет, это ведь просто деталь реализации.