Skip to content

Commit 3295f49

Browse files
authored
feat(llc): Add caching for own capabilities (#47)
1 parent 708cdd9 commit 3295f49

17 files changed

+419
-44
lines changed

packages/stream_feeds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Next release
22
- Update API client with renaming `addReaction` to `addActivityReaction` and `deleteReaction` to `deleteActivityReaction`.
3+
- Update `activity.currentFeed` capabilities when adding or updating activity from websocket events.
34

45
## 0.3.0
56
- [BREAKING] Renamed `AppLifecycleStateProvider` to `LifecycleStateProvider` and `AppLifecycleState` to `LifecycleState`.

packages/stream_feeds/lib/src/client/feeds_client_impl.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import '../models/push_notifications_config.dart';
1515
import '../repository/activities_repository.dart';
1616
import '../repository/app_repository.dart';
1717
import '../repository/bookmarks_repository.dart';
18+
import '../repository/capabilities_repository.dart';
1819
import '../repository/comments_repository.dart';
1920
import '../repository/devices_repository.dart';
2021
import '../repository/feeds_repository.dart';
@@ -163,6 +164,7 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
163164
_feedsRepository = FeedsRepository(feedsApi);
164165
_moderationRepository = ModerationRepository(feedsApi);
165166
_pollsRepository = PollsRepository(feedsApi);
167+
_capabilitiesRepository = CapabilitiesRepository(feedsApi);
166168

167169
moderation = ModerationClient(_moderationRepository);
168170

@@ -193,6 +195,7 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
193195
late final FeedsRepository _feedsRepository;
194196
late final ModerationRepository _moderationRepository;
195197
late final PollsRepository _pollsRepository;
198+
late final CapabilitiesRepository _capabilitiesRepository;
196199

197200
// TODO: Fill this with correct values
198201
late final _systemEnvironmentManager = SystemEnvironmentManager(
@@ -272,6 +275,7 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
272275
pollsRepository: _pollsRepository,
273276
eventsEmitter: events,
274277
onReconnectEmitter: onReconnectEmitter,
278+
capabilitiesRepository: _capabilitiesRepository,
275279
);
276280
}
277281

@@ -306,7 +310,9 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
306310
activitiesRepository: _activitiesRepository,
307311
commentsRepository: _commentsRepository,
308312
pollsRepository: _pollsRepository,
313+
capabilitiesRepository: _capabilitiesRepository,
309314
eventsEmitter: events,
315+
initialActivityData: initialData,
310316
);
311317
}
312318

@@ -316,6 +322,7 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
316322
query: query,
317323
currentUserId: user.id,
318324
activitiesRepository: _activitiesRepository,
325+
capabilitiesRepository: _capabilitiesRepository,
319326
eventsEmitter: events,
320327
);
321328
}

packages/stream_feeds/lib/src/generated/api/model/own_capabilities_batch_response.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class OwnCapabilitiesBatchResponse with _$OwnCapabilitiesBatchResponse {
2323
});
2424

2525
@override
26-
final Map<String, List<String>> capabilities;
26+
final Map<String, List<FeedOwnCapability>> capabilities;
2727

2828
@override
2929
final String duration;

packages/stream_feeds/lib/src/generated/api/model/own_capabilities_batch_response.freezed.dart

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_feeds/lib/src/generated/api/model/own_capabilities_batch_response.g.dart

Lines changed: 40 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_feeds/lib/src/models/activity_data.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ class ActivityData with _$ActivityData {
201201
/// Custom data associated with the activity.
202202
@override
203203
final Map<String, Object?>? custom;
204+
205+
/// Creates a new [ActivityData] instance with the updated ownCapabilities on [currentFeed].
206+
ActivityData copyWithCurrentFeedCapabilities({
207+
required List<FeedOwnCapability> capabilities,
208+
}) {
209+
return copyWith(
210+
currentFeed: currentFeed?.copyWith(
211+
ownCapabilities: capabilities,
212+
),
213+
);
214+
}
204215
}
205216

206217
/// Extension type for activity visibility settings.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'dart:async';
2+
3+
import '../../stream_feeds.dart' as api;
4+
import '../../stream_feeds.dart';
5+
import '../utils/batcher.dart';
6+
7+
class CapabilitiesRepository {
8+
CapabilitiesRepository(api.DefaultApi api) : _api = api;
9+
10+
final api.DefaultApi _api;
11+
final Map<String, List<FeedOwnCapability>> _capabilities = {};
12+
13+
late final Batcher<String, Result<Map<String, List<FeedOwnCapability>>>>
14+
_fetchBatcher = Batcher(
15+
action: (feeds) => _fetchWithRetry(feeds: feeds),
16+
);
17+
18+
Future<Result<Map<String, List<FeedOwnCapability>>>> fetchCapabilities({
19+
required List<String> feeds,
20+
}) async {
21+
final result = await _api.ownCapabilitiesBatch(
22+
ownCapabilitiesBatchRequest: api.OwnCapabilitiesBatchRequest(
23+
feeds: feeds,
24+
),
25+
);
26+
27+
return result.map((response) {
28+
_mergeWithCache(response.capabilities);
29+
return response.capabilities;
30+
});
31+
}
32+
33+
void cacheCapabilitiesForFeeds(List<FeedData> feeds) {
34+
for (final feed in feeds) {
35+
cacheCapabilities(feed.id, feed.ownCapabilities);
36+
}
37+
}
38+
39+
void cacheCapabilities(String feed, List<FeedOwnCapability> capabilities) {
40+
_capabilities[feed] = capabilities;
41+
}
42+
43+
Future<List<FeedOwnCapability>?> getCapabilities(String feed) async {
44+
return _capabilities[feed] ??
45+
(await _fetchBatchedFeedCapabilities(feed)).getOrNull();
46+
}
47+
48+
void dispose() {
49+
_fetchBatcher.dispose();
50+
}
51+
52+
Future<Result<Map<String, List<FeedOwnCapability>>>> _fetchWithRetry({
53+
required List<String> feeds,
54+
bool isRetry = false,
55+
}) async {
56+
final result = await fetchCapabilities(feeds: feeds);
57+
if (result.shouldRetry() && !isRetry) {
58+
await Future<void>.delayed(const Duration(milliseconds: 500));
59+
return _fetchWithRetry(feeds: feeds, isRetry: true);
60+
}
61+
return result;
62+
}
63+
64+
Future<Result<List<FeedOwnCapability>>> _fetchBatchedFeedCapabilities(
65+
String feed,
66+
) async {
67+
final capabilities = await _fetchBatcher.add(feed);
68+
return capabilities.map((capabilities) => capabilities[feed] ?? []);
69+
}
70+
71+
void _mergeWithCache(Map<String, List<FeedOwnCapability>> capabilities) {
72+
_capabilities.addAll(capabilities);
73+
}
74+
}
75+
76+
extension on Result<Map<String, List<FeedOwnCapability>>> {
77+
bool shouldRetry() {
78+
switch (this) {
79+
case api.Success():
80+
return false;
81+
82+
case final api.Failure failure:
83+
final error = failure.error;
84+
if (error is! StreamDioException) {
85+
return false;
86+
}
87+
final exception = error.exception;
88+
if (exception is! HttpClientException) {
89+
return false;
90+
}
91+
final statusCode = exception.statusCode;
92+
if (statusCode == null) {
93+
return false;
94+
}
95+
return statusCode < 100 || statusCode >= 500;
96+
}
97+
}
98+
}

packages/stream_feeds/lib/src/state/activity.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import '../models/request/activity_add_comment_request.dart';
1616
import '../models/request/activity_update_comment_request.dart';
1717
import '../models/threaded_comment_data.dart';
1818
import '../repository/activities_repository.dart';
19+
import '../repository/capabilities_repository.dart';
1920
import '../repository/comments_repository.dart';
2021
import '../repository/polls_repository.dart';
2122
import 'activity_comment_list.dart';
@@ -40,6 +41,7 @@ class Activity with Disposable {
4041
required this.activitiesRepository,
4142
required this.commentsRepository,
4243
required this.pollsRepository,
44+
required this.capabilitiesRepository,
4345
required this.eventsEmitter,
4446
ActivityData? initialActivityData,
4547
}) {
@@ -76,6 +78,7 @@ class Activity with Disposable {
7678
final ActivitiesRepository activitiesRepository;
7779
final CommentsRepository commentsRepository;
7880
final PollsRepository pollsRepository;
81+
final CapabilitiesRepository capabilitiesRepository;
7982

8083
late final ActivityCommentList _commentsList;
8184

@@ -101,7 +104,12 @@ class Activity with Disposable {
101104
Future<Result<ActivityData>> get() async {
102105
final result = await activitiesRepository.getActivity(activityId);
103106

104-
result.onSuccess(_stateNotifier.onActivityUpdated);
107+
result.onSuccess((activity) {
108+
_stateNotifier.onActivityUpdated(activity);
109+
if (activity.currentFeed case final feed?) {
110+
capabilitiesRepository.cacheCapabilitiesForFeeds([feed]);
111+
}
112+
});
105113

106114
// Query the comments as well (state will be updated automatically)
107115
await queryComments();

packages/stream_feeds/lib/src/state/activity_list.dart

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:stream_core/stream_core.dart';
77
import '../models/activity_data.dart';
88
import '../models/query_configuration.dart';
99
import '../repository/activities_repository.dart';
10+
import '../repository/capabilities_repository.dart';
1011
import 'activity_list_state.dart';
1112
import 'event/activity_list_event_handler.dart';
1213
import 'query/activities_query.dart';
@@ -26,6 +27,7 @@ class ActivityList with Disposable {
2627
required this.query,
2728
required this.currentUserId,
2829
required this.activitiesRepository,
30+
required this.capabilitiesRepository,
2931
required this.eventsEmitter,
3032
}) {
3133
_stateNotifier = ActivityListStateNotifier(
@@ -34,13 +36,17 @@ class ActivityList with Disposable {
3436
);
3537

3638
// Attach event handlers for real-time updates
37-
final handler = ActivityListEventHandler(state: _stateNotifier);
39+
final handler = ActivityListEventHandler(
40+
state: _stateNotifier,
41+
capabilitiesRepository: capabilitiesRepository,
42+
);
3843
_eventsSubscription = eventsEmitter.listen(handler.handleEvent);
3944
}
4045

4146
final ActivitiesQuery query;
4247
final String currentUserId;
4348
final ActivitiesRepository activitiesRepository;
49+
final CapabilitiesRepository capabilitiesRepository;
4450

4551
late final ActivityListStateNotifier _stateNotifier;
4652

@@ -93,10 +99,19 @@ class ActivityList with Disposable {
9399
final result = await activitiesRepository.queryActivities(query);
94100

95101
result.onSuccess(
96-
(activitiesData) => _stateNotifier.onQueryMoreActivities(
97-
activitiesData,
98-
QueryConfiguration(filter: query.filter, sort: query.sort),
99-
),
102+
(activitiesData) {
103+
_stateNotifier.onQueryMoreActivities(
104+
activitiesData,
105+
QueryConfiguration(filter: query.filter, sort: query.sort),
106+
);
107+
108+
capabilitiesRepository.cacheCapabilitiesForFeeds(
109+
activitiesData.items
110+
.map((activity) => activity.currentFeed)
111+
.nonNulls
112+
.toList(),
113+
);
114+
},
100115
);
101116

102117
return result.map((activitiesData) => activitiesData.items);

0 commit comments

Comments
 (0)