Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/stream_chat_persistence/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Upcoming

🚀 Performance

- Reduce the number of DB reads in the `ChatPersistenceClient.getChannelStates` method.

## 9.24.0

- Updated `stream_chat` dependency to [`9.24.0`](https://pub.dev/packages/stream_chat/changelog).
Expand Down
4 changes: 2 additions & 2 deletions packages/stream_chat_persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
- [Flutter Offline Docs](https://getstream.io/chat/docs/flutter-dart/flutter_offline/)

This package provides a persistence client for fetching and saving chat data locally.
Stream Chat Persistence uses [Moor](https://github.com/simolus3/moor) as a disk cache.
Stream Chat Persistence uses [Drift](https://github.com/simolus3/drift) as a disk cache.

### Changelog

Expand Down Expand Up @@ -52,7 +52,7 @@ And you are ready to go...

## Flutter Web

Due to Moor web (for offline storage) you need to include the sql.js library:
Due to Drift web (for offline storage) you need to include the sql.js library:

```html
<!doctype html>
Expand Down
28 changes: 28 additions & 0 deletions packages/stream_chat_persistence/lib/src/dao/member_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ class MemberDao extends DatabaseAccessor<DriftChatDatabase>
return memberEntity.toMember(user: userEntity.toUser());
}).get();

/// Returns the membership row for [userId] in each channel listed in [cids],
/// keyed by `channelCid`.
///
/// Channels in [cids] without a membership row for [userId] are absent from
/// the result. Executes a single `WHERE channel_cid IN (...) AND user_id = ?`
/// query.
Future<Map<String, Member>> getMembershipsForChannels(
List<String> cids,
String userId,
) async {
if (cids.isEmpty) return const {};

final rows = await (select(members).join([
leftOuterJoin(users, members.userId.equalsExp(users.id)),
])
..where(
members.channelCid.isIn(cids) & members.userId.equals(userId),
))
.get();

return {
for (final row in rows)
row.readTable(members).channelCid: row.readTable(members).toMember(
user: row.readTableOrNull(users)?.toUser(),
),
};
}

/// Updates all the members using the new [memberList] data
Future<void> updateMembers(String cid, List<Member> memberList) =>
bulkUpdateMembers({cid: memberList});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export 'shared/shared_db.dart';

part 'drift_chat_database.g.dart';

/// A chat database implemented using moor
/// A chat database implemented using drift
@DriftDatabase(
tables: [
Channels,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SharedDB {
return DriftChatDatabase(
userId,
DatabaseConnection.delayed(Future(() async {
final isolate = await _createMoorIsolate(
final isolate = await _createDriftIsolate(
dbName,
logStatements: logStatements,
);
Expand Down Expand Up @@ -68,13 +68,13 @@ class SharedDB {
File(request.targetPath),
logStatements: request.logStatements,
));
final moorIsolate = DriftIsolate.inCurrent(
final driftIsolate = DriftIsolate.inCurrent(
() => DatabaseConnection(executor),
);
request.sendMoorIsolate.send(moorIsolate);
request.sendDriftIsolate.send(driftIsolate);
}

static Future<DriftIsolate> _createMoorIsolate(
static Future<DriftIsolate> _createDriftIsolate(
String dbName, {
bool logStatements = false,
}) async {
Expand All @@ -97,12 +97,12 @@ class SharedDB {

class _IsolateStartRequest {
const _IsolateStartRequest(
this.sendMoorIsolate,
this.sendDriftIsolate,
this.targetPath, {
this.logStatements = false,
});

final SendPort sendMoorIsolate;
final SendPort sendDriftIsolate;
final String targetPath;
final bool logStatements;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// coverage:ignore-file
import 'package:drift/drift.dart';

/// Represents a [ChannelQueries] table in [MoorChatDatabase].
/// Represents a [ChannelQueries] table in [DriftChatDatabase].
@DataClassName('ChannelQueryEntity')
class ChannelQueries extends Table {
/// The unique hash of this query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/converter.dart';

/// Represents a [Channels] table in [MoorChatDatabase].
/// Represents a [Channels] table in [DriftChatDatabase].
@DataClassName('ChannelEntity')
class Channels extends Table {
/// The id of this channel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';

/// Represents a [ConnectionEvents] table in [MoorChatDatabase].
/// Represents a [ConnectionEvents] table in [DriftChatDatabase].
@DataClassName('ConnectionEventEntity')
class ConnectionEvents extends Table {
/// event id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:stream_chat_persistence/src/converter/converter.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';

/// Represents a [DraftMessages] table in [MoorChatDatabase].
/// Represents a [DraftMessages] table in [DriftChatDatabase].
@DataClassName('DraftMessageEntity')
class DraftMessages extends Table {
/// The message id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart';

/// Represents a [Members] table in [MoorChatDatabase].
/// Represents a [Members] table in [DriftChatDatabase].
@DataClassName('MemberEntity')
class Members extends Table {
/// The interested user id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:stream_chat_persistence/src/converter/map_converter.dart';
import 'package:stream_chat_persistence/src/converter/reaction_groups_converter.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart';

/// Represents a [Messages] table in [MoorChatDatabase].
/// Represents a [Messages] table in [DriftChatDatabase].
@DataClassName('MessageEntity')
class Messages extends Table {
/// The message id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/entity/pinned_messages.dart';
import 'package:stream_chat_persistence/src/entity/reactions.dart';

/// Represents a [PinnedMessageReactions] table in [MoorChatDatabase].
/// Represents a [PinnedMessageReactions] table in [DriftChatDatabase].
@DataClassName('PinnedMessageReactionEntity')
class PinnedMessageReactions extends Reactions {
/// The messageId to which the reaction belongs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';

/// Represents a [PinnedMessages] table in [MoorChatDatabase].
/// Represents a [PinnedMessages] table in [DriftChatDatabase].
@DataClassName('PinnedMessageEntity')
class PinnedMessages extends Messages {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/entity/entity.dart';

/// Represents a [PollVotes] table in [MoorChatDatabase].
/// Represents a [PollVotes] table in [DriftChatDatabase].
@DataClassName('PollVoteEntity')
class PollVotes extends Table {
/// The unique identifier of the poll vote.
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat_persistence/lib/src/entity/polls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:stream_chat_persistence/src/converter/list_converter.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
import 'package:stream_chat_persistence/src/converter/voting_visibility_converter.dart';

/// Represents a [Polls] table in [MoorChatDatabase].
/// Represents a [Polls] table in [DriftChatDatabase].
@DataClassName('PollEntity')
class Polls extends Table {
/// The unique identifier of the poll.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';

/// Represents a [Reactions] table in [MoorChatDatabase].
/// Represents a [Reactions] table in [DriftChatDatabase].
@DataClassName('ReactionEntity')
class Reactions extends Table {
/// The id of the user that sent the reaction
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat_persistence/lib/src/entity/reads.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart';

/// Represents a [Reads] table in [MoorChatDatabase].
/// Represents a [Reads] table in [DriftChatDatabase].
@DataClassName('ReadEntity')
class Reads extends Table {
/// Date of the read event
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat_persistence/lib/src/entity/users.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';

/// Represents a [Users] table in [MoorChatDatabase].
/// Represents a [Users] table in [DriftChatDatabase].
@DataClassName('UserEntity')
class Users extends Table {
/// User id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,29 +270,42 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
assert(_debugIsConnected, '');
_logger.info('getChannelStates');

final channels = await db!.channelQueryDao.getChannels(filter: filter);

final channelStates = await Future.wait(
channels.map((e) => getChannelStateByCid(e.cid)),
);

// Sort the channel states
if (channelStateSort != null && channelStateSort.isNotEmpty) {
channelStates.sort(channelStateSort.compare);
// 1) Lightweight load — channel rows + createdBy user only.
final channelModels = await db!.channelQueryDao.getChannels(filter: filter);

// 2) Wrap each model in a sort envelope. No state loaded yet.
var envelopes = channelModels
.map((m) => ChannelState(channel: m))
.toList(growable: false);

// 3) If sort uses `pinnedAt`, preload the current user's memberships in
// one batched query and attach them to the envelopes.
final clientUserId = userId;
if (clientUserId != null && _sortRequiresMembership(channelStateSort)) {
envelopes = await _attachMemberships(envelopes, clientUserId);
}

// Apply offset
if (paginationParams?.offset case final paginationOffset?) {
final clampedOffset = paginationOffset.clamp(0, channelStates.length);
channelStates.removeRange(0, clampedOffset);
// 4) Sort using the existing comparator — same logic as today, just on
// envelopes instead of fully-hydrated states.
if (channelStateSort != null && channelStateSort.isNotEmpty) {
envelopes.sort(channelStateSort.compare);
}

// Apply limit
if (paginationParams?.limit case final paginationLimit?) {
return channelStates.take(paginationLimit).toList();
}
// 5) Slice the page.
final total = envelopes.length;
final offset = (paginationParams?.offset ?? 0).clamp(0, total);
final limit = paginationParams?.limit ?? (total - offset);
final pagedCids =
envelopes.skip(offset).take(limit).map((s) => s.channel!.cid).toList();

return channelStates;
// 6) Hydrate ONLY the page. Pass messagePagination so we don't load every
// cached message for every channel (matches API default).
return Future.wait(pagedCids.map(
(cid) => getChannelStateByCid(
cid,
messagePagination: const PaginationParams(limit: 25),
),
));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

@override
Expand Down Expand Up @@ -481,4 +494,25 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
db = null;
}
}

bool _sortRequiresMembership(SortOrder<ChannelState>? sort) =>
sort?.any((opt) => opt.field == ChannelSortKey.pinnedAt) ?? false;

Future<List<ChannelState>> _attachMemberships(
List<ChannelState> envelopes,
String currentUserId,
) async {
final cids = envelopes
.map((s) => s.channel?.cid)
.whereType<String>()
.toList(growable: false);
final memberships = await db!.memberDao.getMembershipsForChannels(
cids,
currentUserId,
);
return [
for (final s in envelopes)
s.copyWith(membership: memberships[s.channel?.cid]),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DatabaseConnection _backgroundConnection() =>

void main() {
test(
'default constructor should create a new instance of MoorChatDatabase',
'default constructor should create a new instance of DriftChatDatabase',
() async {
const userId = 'testUserId';
final executor = NativeDatabase.memory();
Expand All @@ -24,7 +24,7 @@ void main() {
);

test(
'connect constructor should create a new instance of MoorChatDatabase',
'connect constructor should create a new instance of DriftChatDatabase',
() async {
const userId = 'testUserId';
final isolate = await DriftIsolate.spawn(_backgroundConnection);
Expand Down
Loading
Loading