Merge pull request #1583 from GetStream/feat/offline-connect

This commit is contained in:
Sahil Kumar
2023-06-06 16:25:55 +05:30
committed by GitHub
10 changed files with 240 additions and 55 deletions
+3
View File
@@ -14,6 +14,9 @@
✅ Added ✅ Added
- Added support for `ChatPersistenceClient.isConnected` for checking if the client is connected to the database. - Added support for `ChatPersistenceClient.isConnected` for checking if the client is connected to the database.
- Added support for `ChatPersistenceClient.userId` for getting the current connected user id.
- Added two new methods `ChatPersistenceClient.disconnect` and `ChatPersistenceClient.connect` for disconnecting and
connecting to the database.
## 6.1.0 ## 6.1.0
@@ -1379,13 +1379,14 @@ class Channel {
this.state?.updateChannelState(updatedState); this.state?.updateChannelState(updatedState);
return updatedState; return updatedState;
} catch (e) { } catch (e) {
if (!_client.persistenceEnabled) { if (_client.persistenceEnabled) {
rethrow; return _client.chatPersistenceClient!.getChannelStateByCid(
cid!,
messagePagination: messagesPagination,
);
} }
return _client.chatPersistenceClient!.getChannelStateByCid(
cid!, rethrow;
messagePagination: messagesPagination,
);
} }
} }
@@ -1841,9 +1842,7 @@ class ChannelClientState {
/// [isUpToDate] flag count as a stream. /// [isUpToDate] flag count as a stream.
Stream<bool> get isUpToDateStream => _isUpToDateController.stream; Stream<bool> get isUpToDateStream => _isUpToDateController.stream;
final _isUpToDateController = BehaviorSubject.seeded(true);
final BehaviorSubject<bool> _isUpToDateController =
BehaviorSubject.seeded(true);
/// The retry queue associated to this channel. /// The retry queue associated to this channel.
late final RetryQueue _retryQueue; late final RetryQueue _retryQueue;
+76 -41
View File
@@ -125,10 +125,6 @@ class StreamChatClient {
final _tokenManager = TokenManager(); final _tokenManager = TokenManager();
final _connectionIdManager = ConnectionIdManager(); final _connectionIdManager = ConnectionIdManager();
set chatPersistenceClient(ChatPersistenceClient? value) {
_originalChatPersistenceClient = value;
}
/// Default user agent for all requests /// Default user agent for all requests
static String defaultUserAgent = static String defaultUserAgent =
'stream-chat-dart-client-${CurrentPlatform.name}'; 'stream-chat-dart-client-${CurrentPlatform.name}';
@@ -139,15 +135,15 @@ class StreamChatClient {
/// The current package version /// The current package version
static const packageVersion = PACKAGE_VERSION; static const packageVersion = PACKAGE_VERSION;
ChatPersistenceClient? _originalChatPersistenceClient;
/// Chat persistence client /// Chat persistence client
ChatPersistenceClient? get chatPersistenceClient => _chatPersistenceClient; ChatPersistenceClient? chatPersistenceClient;
ChatPersistenceClient? _chatPersistenceClient; /// Returns `True` if the [chatPersistenceClient] is available and connected.
/// Otherwise, returns `False`.
/// Whether the chat persistence is available or not bool get persistenceEnabled {
bool get persistenceEnabled => _chatPersistenceClient != null; final client = chatPersistenceClient;
return client != null && client.isConnected;
}
late final RetryPolicy _retryPolicy; late final RetryPolicy _retryPolicy;
@@ -324,20 +320,27 @@ class StreamChatClient {
final ownUser = OwnUser.fromUser(user); final ownUser = OwnUser.fromUser(user);
state.currentUser = ownUser; state.currentUser = ownUser;
if (!connectWebSocket) return ownUser;
try { try {
if (_originalChatPersistenceClient != null) { // Connect to persistence client if its set.
_chatPersistenceClient = _originalChatPersistenceClient; if (chatPersistenceClient != null) {
await _chatPersistenceClient!.connect(ownUser.id); await openPersistenceConnection(ownUser);
} }
final connectedUser = await openConnection(
includeUserDetailsInConnectCall: true, // Connect to websocket if [connectWebSocket] is true.
); //
return state.currentUser = connectedUser; // This is useful when you want to connect to websocket
// at a later stage or use the client in connection-less mode.
if (connectWebSocket) {
final connectedUser = await openConnection(
includeUserDetailsInConnectCall: true,
);
state.currentUser = connectedUser;
}
return state.currentUser!;
} catch (e, stk) { } catch (e, stk) {
if (e is StreamWebSocketError && e.isRetriable) { if (e is StreamWebSocketError && e.isRetriable) {
final event = await _chatPersistenceClient?.getConnectionInfo(); final event = await chatPersistenceClient?.getConnectionInfo();
if (event != null) return ownUser.merge(event.me); if (event != null) return ownUser.merge(event.me);
} }
logger.severe('error connecting user : ${ownUser.id}', e, stk); logger.severe('error connecting user : ${ownUser.id}', e, stk);
@@ -345,6 +348,40 @@ class StreamChatClient {
} }
} }
/// Connects the [chatPersistenceClient] to the given [user].
Future<void> openPersistenceConnection(User user) async {
final client = chatPersistenceClient;
if (client == null) {
throw const StreamChatError('Chat persistence client is not set');
}
if (client.isConnected) {
// If the persistence client is already connected to the userId,
// we don't need to connect again.
if (client.userId == user.id) return;
throw const StreamChatError('''
Chat persistence client is already connected to a different user,
please close the connection before connecting a new one.''');
}
// Connect the persistence client to the userId.
return client.connect(user.id);
}
/// Disconnects the [chatPersistenceClient] from the current user.
Future<void> closePersistenceConnection({bool flush = false}) async {
final client = chatPersistenceClient;
// If the persistence client is never connected, we don't need to close it.
if (client == null || !client.isConnected) {
logger.info('Chat persistence client is not connected');
return;
}
// Disconnect the persistence client.
return client.disconnect(flush: flush);
}
/// Creates a new WebSocket connection with the current user. /// Creates a new WebSocket connection with the current user.
/// If [includeUserDetailsInConnectCall] is true it will include the current /// If [includeUserDetailsInConnectCall] is true it will include the current
/// user details in the connect call. /// user details in the connect call.
@@ -422,7 +459,7 @@ class StreamChatClient {
final connectionId = event.connectionId; final connectionId = event.connectionId;
if (connectionId != null) { if (connectionId != null) {
_connectionIdManager.setConnectionId(connectionId); _connectionIdManager.setConnectionId(connectionId);
_chatPersistenceClient?.updateConnectionInfo(event); chatPersistenceClient?.updateConnectionInfo(event);
} }
} }
@@ -460,9 +497,9 @@ class StreamChatClient {
// channels are empty, assuming it's a fresh start // channels are empty, assuming it's a fresh start
// and making sure `lastSyncAt` is initialized // and making sure `lastSyncAt` is initialized
if (persistenceEnabled) { if (persistenceEnabled) {
final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt(); final lastSyncAt = await chatPersistenceClient?.getLastSyncAt();
if (lastSyncAt == null) { if (lastSyncAt == null) {
await _chatPersistenceClient?.updateLastSyncAt(DateTime.now()); await chatPersistenceClient?.updateLastSyncAt(DateTime.now());
} }
} }
} }
@@ -493,13 +530,12 @@ class StreamChatClient {
/// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled] /// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled]
Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) { Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) {
return synchronized(() async { return synchronized(() async {
final channels = cids ?? await _chatPersistenceClient?.getChannelCids(); final channels = cids ?? await chatPersistenceClient?.getChannelCids();
if (channels == null || channels.isEmpty) { if (channels == null || channels.isEmpty) {
return; return;
} }
final syncAt = final syncAt = lastSyncAt ?? await chatPersistenceClient?.getLastSyncAt();
lastSyncAt ?? await _chatPersistenceClient?.getLastSyncAt();
if (syncAt == null) { if (syncAt == null) {
return; return;
} }
@@ -520,7 +556,7 @@ class StreamChatClient {
final now = DateTime.now(); final now = DateTime.now();
_lastSyncedAt = now; _lastSyncedAt = now;
_chatPersistenceClient?.updateLastSyncAt(now); chatPersistenceClient?.updateLastSyncAt(now);
} catch (e, stk) { } catch (e, stk) {
logger.severe('Error during sync', e, stk); logger.severe('Error during sync', e, stk);
} }
@@ -679,7 +715,7 @@ class StreamChatClient {
final updateData = _mapChannelStateToChannel(channels); final updateData = _mapChannelStateToChannel(channels);
await _chatPersistenceClient?.updateChannelQueries( await chatPersistenceClient?.updateChannelQueries(
filter, filter,
channels.map((c) => c.channel!.cid).toList(), channels.map((c) => c.channel!.cid).toList(),
clearQueryCache: paginationParams.offset == 0, clearQueryCache: paginationParams.offset == 0,
@@ -698,7 +734,7 @@ class StreamChatClient {
List<SortOption<ChannelState>>? channelStateSort, List<SortOption<ChannelState>>? channelStateSort,
PaginationParams paginationParams = const PaginationParams(), PaginationParams paginationParams = const PaginationParams(),
}) async { }) async {
final offlineChannels = (await _chatPersistenceClient?.getChannelStates( final offlineChannels = (await chatPersistenceClient?.getChannelStates(
filter: filter, filter: filter,
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
sort: sort, sort: sort,
@@ -1362,7 +1398,7 @@ class StreamChatClient {
final response = final response =
await _chatApi.message.deleteMessage(messageId, hard: hard); await _chatApi.message.deleteMessage(messageId, hard: hard);
if (hard == true) { if (hard == true) {
await _chatPersistenceClient?.deleteMessageById(messageId); await chatPersistenceClient?.deleteMessageById(messageId);
} }
return response; return response;
} }
@@ -1468,34 +1504,33 @@ class StreamChatClient {
Future<void> disconnectUser({bool flushChatPersistence = false}) async { Future<void> disconnectUser({bool flushChatPersistence = false}) async {
logger.info('Disconnecting user : ${state.currentUser?.id}'); logger.info('Disconnecting user : ${state.currentUser?.id}');
// resetting state // resetting state.
state.dispose(); state.dispose();
state = ClientState(this); state = ClientState(this);
_lastSyncedAt = null; _lastSyncedAt = null;
// resetting credentials // resetting credentials.
_tokenManager.reset(); _tokenManager.reset();
_connectionIdManager.reset(); _connectionIdManager.reset();
// disconnecting persistence client // closing persistence connection.
await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); await closePersistenceConnection(flush: flushChatPersistence);
_chatPersistenceClient = null;
// closing web-socket connection // closing web-socket connection
closeConnection(); return closeConnection();
} }
/// Call this function to dispose the client /// Call this function to dispose the client
Future<void> dispose() async { Future<void> dispose() async {
logger.info('Disposing new StreamChatClient'); logger.info('Disposing new StreamChatClient');
// disposing state // disposing state.
state.dispose(); state.dispose();
// disconnecting persistence client // closing persistence connection.
await _chatPersistenceClient?.disconnect(); await closePersistenceConnection();
// closing web-socket connection // closing web-socket connection.
closeConnection(); closeConnection();
await _eventController.close(); await _eventController.close();
@@ -17,6 +17,11 @@ abstract class ChatPersistenceClient {
/// Whether the connection is established. /// Whether the connection is established.
bool get isConnected; bool get isConnected;
/// The current user id to which the client is connected.
///
/// Returns `null` if the client is not connected.
String? get userId;
/// Creates a new connection to the client /// Creates a new connection to the client
Future<void> connect(String userId); Future<void> connect(String userId);
@@ -2544,4 +2544,124 @@ void main() {
}, },
); );
}); });
group('PersistenceConnectionTests', () {
const apiKey = 'test-api-key';
late final api = FakeChatApi();
late final ws = FakeWebSocket();
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
late StreamChatClient client;
setUp(() async {
client = StreamChatClient(apiKey, chatApi: api, ws: ws);
expect(client.persistenceEnabled, isFalse);
});
tearDown(() {
client.chatPersistenceClient = null;
expect(client.persistenceEnabled, isFalse);
client.dispose();
});
test('openPersistenceConnection connects the client to the user', () async {
client.chatPersistenceClient = MockPersistenceClient();
await client.openPersistenceConnection(user);
expect(client.persistenceEnabled, isTrue);
});
test(
'''multiple call to openPersistenceConnection does not throws an error if already connected to the same user''',
() async {
client.chatPersistenceClient = MockPersistenceClient();
await client.openPersistenceConnection(user);
expect(client.persistenceEnabled, isTrue);
await expectLater(client.openPersistenceConnection(user), completes);
await expectLater(client.openPersistenceConnection(user), completes);
await expectLater(client.openPersistenceConnection(user), completes);
},
);
test(
'''openPersistenceConnection throws an error if client is already connected to a different user''',
() async {
client.chatPersistenceClient = MockPersistenceClient();
await client.openPersistenceConnection(user);
expect(client.persistenceEnabled, isTrue);
await expectLater(
client.openPersistenceConnection(user.copyWith(id: 'new-id')),
throwsA(const TypeMatcher<StreamChatError>()),
);
},
);
test(
'''openPersistenceConnection throws an error if chatPersistenceClient is not set''',
() async {
await expectLater(
client.openPersistenceConnection(user),
throwsA(const TypeMatcher<StreamChatError>()),
);
},
);
test('closePersistenceConnection disconnects the client', () async {
client.chatPersistenceClient = MockPersistenceClient();
await client.openPersistenceConnection(user);
expect(client.persistenceEnabled, isTrue);
await client.closePersistenceConnection();
expect(client.persistenceEnabled, isFalse);
});
test(
'''closePersistenceConnection compeletes normally if chatPersistenceClient is not connected''',
() async {
client.chatPersistenceClient = MockPersistenceClient();
expect(client.chatPersistenceClient!.isConnected, isFalse);
await expectLater(client.closePersistenceConnection(), completes);
},
);
test(
'''closePersistenceConnection completes normally if chatPersistenceClient is not set''',
() async {
expect(client.persistenceEnabled, isFalse);
await expectLater(client.closePersistenceConnection(), completes);
},
);
test(
'''connectUser completes normally if the persistence connection is already connected to the same user''',
() async {
client.chatPersistenceClient = MockPersistenceClient();
await client.openPersistenceConnection(user);
expect(client.persistenceEnabled, isTrue);
await expectLater(
client.connectUser(user, token, connectWebSocket: false),
completes,
);
},
);
test(
'''connectUser should throw if the persistence connection if already connected to a different user''',
() async {
client.chatPersistenceClient = MockPersistenceClient();
await client.openPersistenceConnection(user.copyWith(id: 'new-id'));
expect(client.persistenceEnabled, isTrue);
await expectLater(
client.connectUser(user, token, connectWebSocket: false),
throwsA(const TypeMatcher<StreamChatError>()),
);
},
);
});
} }
@@ -15,6 +15,9 @@ class TestPersistenceClient extends ChatPersistenceClient {
@override @override
bool get isConnected => throw UnimplementedError(); bool get isConnected => throw UnimplementedError();
@override
String? get userId => throw UnimplementedError();
@override @override
Future<void> connect(String userId) => throw UnimplementedError(); Future<void> connect(String userId) => throw UnimplementedError();
+18 -3
View File
@@ -64,11 +64,26 @@ class MockAttachmentFileUploader extends Mock
implements AttachmentFileUploader {} implements AttachmentFileUploader {}
class MockPersistenceClient extends Mock implements ChatPersistenceClient { class MockPersistenceClient extends Mock implements ChatPersistenceClient {
@override String? _userId;
Future<void> connect(String userId) => Future.value(); bool _isConnected = false;
@override @override
Future<void> disconnect({bool flush = false}) => Future.value(); bool get isConnected => _isConnected;
@override
String? get userId => _userId;
@override
Future<void> connect(String userId) async {
_userId = userId;
_isConnected = true;
}
@override
Future<void> disconnect({bool flush = false}) async {
_userId = null;
_isConnected = false;
}
} }
class MockStreamChatClient extends Mock implements StreamChatClient { class MockStreamChatClient extends Mock implements StreamChatClient {
@@ -4,6 +4,8 @@
- [[#1422]](https://github.com/GetStream/stream-chat-flutter/issues/1422) Removed default values - [[#1422]](https://github.com/GetStream/stream-chat-flutter/issues/1422) Removed default values
from `UserEntity` `createdAt` and `updatedAt` fields. from `UserEntity` `createdAt` and `updatedAt` fields.
- Updated `stream_chat` dependency to [`6.2.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`6.2.0`](https://pub.dev/packages/stream_chat/changelog).
- Added support for `StreamChatPersistenceClient.openPersistenceConnection`
and `StreamChatPersistenceClient.closePersistenceConnection` for opening and closing the database connection.
## 6.1.0 ## 6.1.0
@@ -82,6 +82,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override @override
bool get isConnected => db != null; bool get isConnected => db != null;
@override
String? get userId => db?.userId;
@override @override
Future<void> connect( Future<void> connect(
String userId, { String userId, {
@@ -20,7 +20,7 @@ void main() {
await client.connect(userId, databaseProvider: testDatabaseProvider); await client.connect(userId, databaseProvider: testDatabaseProvider);
expect(client.isConnected, true); expect(client.isConnected, true);
expect(client.db, isA<DriftChatDatabase>()); expect(client.db, isA<DriftChatDatabase>());
expect(client.db!.userId, userId); expect(client.userId, userId);
addTearDown(() async { addTearDown(() async {
await client.disconnect(); await client.disconnect();
@@ -33,7 +33,7 @@ void main() {
await client.connect(userId, databaseProvider: testDatabaseProvider); await client.connect(userId, databaseProvider: testDatabaseProvider);
expect(client.isConnected, true); expect(client.isConnected, true);
expect(client.db, isA<DriftChatDatabase>()); expect(client.db, isA<DriftChatDatabase>());
expect(client.db!.userId, userId); expect(client.userId, userId);
expect( expect(
() => client.connect(userId, databaseProvider: testDatabaseProvider), () => client.connect(userId, databaseProvider: testDatabaseProvider),
throwsException, throwsException,