Merge pull request #520 from GetStream/feat/connectionLessClient

feat!: connection less client [CDS-335]
This commit is contained in:
Salvatore Giordano
2021-07-08 10:38:07 +02:00
committed by GitHub
6 changed files with 261 additions and 44 deletions
+52 -16
View File
@@ -220,27 +220,53 @@ class StreamChatClient {
/// Connects the current user, this triggers a connection to the API. /// Connects the current user, this triggers a connection to the API.
/// It returns a [Future] that resolves when the connection is setup. /// It returns a [Future] that resolves when the connection is setup.
Future<Event> connectUser(User user, String token) => /// Pass [connectWebSocket]: false, if you want to connect to websocket
_connectUser(user, token: Token.fromRawValue(token)); /// at a later stage or use the client in connection-less mode
Future<OwnUser> connectUser(
User user,
String token, {
bool connectWebSocket = true,
}) =>
_connectUser(
user,
token: Token.fromRawValue(token),
connectWebSocket: connectWebSocket,
);
/// Connects the current user using the [tokenProvider] to fetch the token. /// Connects the current user using the [tokenProvider] to fetch the token.
/// It returns a [Future] that resolves when the connection is setup. /// It returns a [Future] that resolves when the connection is setup.
Future<Event> connectUserWithProvider( Future<OwnUser> connectUserWithProvider(
User user, TokenProvider tokenProvider) => User user,
_connectUser(user, provider: tokenProvider); TokenProvider tokenProvider, {
bool connectWebSocket = true,
}) =>
_connectUser(
user,
provider: tokenProvider,
connectWebSocket: connectWebSocket,
);
/// Connects the current user with an anonymous id, this triggers a connection /// Connects the current user with an anonymous id, this triggers a connection
/// to the API. It returns a [Future] that resolves when the connection is /// to the API. It returns a [Future] that resolves when the connection is
/// setup. /// setup.
Future<Event> connectAnonymousUser() async { Future<OwnUser> connectAnonymousUser({
bool connectWebSocket = true,
}) async {
final token = Token.anonymous(); final token = Token.anonymous();
final user = OwnUser(id: token.userId); final user = OwnUser(id: token.userId);
return _connectUser(user, token: token); return _connectUser(
user,
token: token,
connectWebSocket: connectWebSocket,
);
} }
/// Connects the current user as guest, this triggers a connection to the API. /// Connects the current user as guest, this triggers a connection to the API.
/// It returns a [Future] that resolves when the connection is setup. /// It returns a [Future] that resolves when the connection is setup.
Future<Event> connectGuestUser(User user) async { Future<OwnUser> connectGuestUser(
User user, {
bool connectWebSocket = true,
}) async {
final userId = user.id; final userId = user.id;
final anonymousToken = Token.anonymous(userId: userId); final anonymousToken = Token.anonymous(userId: userId);
@@ -253,13 +279,18 @@ class StreamChatClient {
_tokenManager.reset(); _tokenManager.reset();
final guestUserToken = Token.fromRawValue(guestUser.accessToken); final guestUserToken = Token.fromRawValue(guestUser.accessToken);
return _connectUser(guestUser.user, token: guestUserToken); return _connectUser(
guestUser.user,
token: guestUserToken,
connectWebSocket: connectWebSocket,
);
} }
Future<Event> _connectUser( Future<OwnUser> _connectUser(
User user, { User user, {
Token? token, Token? token,
TokenProvider? provider, TokenProvider? provider,
bool connectWebSocket = true,
}) async { }) async {
if (_ws.connectionCompleter?.isCompleted == false) { if (_ws.connectionCompleter?.isCompleted == false) {
throw const StreamChatError( throw const StreamChatError(
@@ -268,7 +299,7 @@ class StreamChatClient {
); );
} }
logger.info('connecting user : ${user.id}'); logger.info('setting user : ${user.id}');
await _tokenManager.setTokenOrProvider( await _tokenManager.setTokenOrProvider(
user.id, user.id,
@@ -279,17 +310,21 @@ class StreamChatClient {
final ownUser = OwnUser.fromUser(user); final ownUser = OwnUser.fromUser(user);
state.user = ownUser; state.user = ownUser;
if (!connectWebSocket) {
return ownUser;
}
try { try {
if (_originalChatPersistenceClient != null) { if (_originalChatPersistenceClient != null) {
_chatPersistenceClient = _originalChatPersistenceClient; _chatPersistenceClient = _originalChatPersistenceClient;
await _chatPersistenceClient!.connect(ownUser.id); await _chatPersistenceClient!.connect(ownUser.id);
} }
final event = await openConnection(); final res = await openConnection();
return event; return res;
} 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 event; if (event != null) return event.me?.merge(ownUser) ?? ownUser;
} }
logger.severe('error connecting user : ${ownUser.id}', e, stk); logger.severe('error connecting user : ${ownUser.id}', e, stk);
rethrow; rethrow;
@@ -297,7 +332,7 @@ class StreamChatClient {
} }
/// Creates a new WebSocket connection with the current user. /// Creates a new WebSocket connection with the current user.
Future<Event> openConnection() async { Future<OwnUser> openConnection() async {
assert( assert(
state.user != null, state.user != null,
'User is not set on client, ' 'User is not set on client, '
@@ -327,7 +362,8 @@ class StreamChatClient {
_ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler); _ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler);
try { try {
return await _ws.connect(user); final event = await _ws.connect(user);
return event.me?.merge(user) ?? user;
} catch (e, stk) { } catch (e, stk) {
logger.severe('error connecting ws', e, stk); logger.severe('error connecting ws', e, stk);
rethrow; rethrow;
@@ -3,6 +3,7 @@ import 'package:stream_chat/src/core/models/device.dart';
import 'package:stream_chat/src/core/models/mute.dart'; import 'package:stream_chat/src/core/models/mute.dart';
import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:stream_chat/stream_chat.dart';
part 'own_user.g.dart'; part 'own_user.g.dart';
@@ -25,6 +26,7 @@ class OwnUser extends User {
bool online = false, bool online = false,
Map<String, Object?> extraData = const {}, Map<String, Object?> extraData = const {},
bool banned = false, bool banned = false,
List<String> teams = const [],
}) : super( }) : super(
id: id, id: id,
role: role, role: role,
@@ -34,6 +36,7 @@ class OwnUser extends User {
online: online, online: online,
extraData: extraData, extraData: extraData,
banned: banned, banned: banned,
teams: teams,
); );
/// Create a new instance from a json /// Create a new instance from a json
@@ -50,8 +53,69 @@ class OwnUser extends User {
online: user.online, online: user.online,
banned: user.banned, banned: user.banned,
extraData: user.extraData, extraData: user.extraData,
teams: user.teams,
); );
/// Creates a copy of [OwnUser] with specified attributes overridden.
@override
OwnUser copyWith({
String? id,
String? role,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastActive,
bool? online,
Map<String, Object?>? extraData,
bool? banned,
List<String>? teams,
List<Mute>? channelMutes,
List<Device>? devices,
List<Mute>? mutes,
int? totalUnreadCount,
int? unreadChannels,
}) =>
OwnUser(
id: id ?? this.id,
banned: banned ?? this.banned,
role: role ?? this.role,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastActive: lastActive ?? this.lastActive,
online: online ?? this.online,
extraData: extraData ?? this.extraData,
teams: teams ?? this.teams,
channelMutes: channelMutes ?? this.channelMutes,
devices: devices ?? this.devices,
mutes: mutes ?? this.mutes,
totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount,
unreadChannels: unreadChannels ?? this.unreadChannels,
);
/// Returns a new [OwnUser] that is a combination of this ownUser
/// and the given [other] ownUser.
OwnUser merge(OwnUser? other) {
if (other == null) {
return this;
}
return copyWith(
banned: other.banned,
channelMutes: other.channelMutes,
createdAt: other.createdAt,
devices: other.devices,
extraData: other.extraData,
id: other.id,
lastActive: other.lastActive,
mutes: other.mutes,
online: other.online,
role: other.role,
teams: other.teams,
totalUnreadCount: other.totalUnreadCount,
unreadChannels: other.unreadChannels,
updatedAt: other.updatedAt,
);
}
/// List of user devices /// List of user devices
@JsonKey(includeIfNull: false, defaultValue: <Device>[]) @JsonKey(includeIfNull: false, defaultValue: <Device>[])
final List<Device> devices; final List<Device> devices;
@@ -47,9 +47,10 @@ class User extends Equatable {
/// User role /// User role
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
toJson: Serializer.readOnly, toJson: Serializer.readOnly,
defaultValue: <String>[]) defaultValue: <String>[],
)
final List<String> teams; final List<String> teams;
/// Date of user creation /// Date of user creation
@@ -64,9 +64,7 @@ void main() {
final res = await client.connectUser(user, token); final res = await client.connectUser(user, token);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.type, event.type); expect(res, isSameUserAs(user));
expect(res.connectionId, event.connectionId);
expect(res.me, isSameUserAs(user));
}); });
test('`.connectUserWithProvider` should work fine', () async { test('`.connectUserWithProvider` should work fine', () async {
@@ -93,9 +91,7 @@ void main() {
final res = await client.connectUserWithProvider(user, tokenProvider); final res = await client.connectUserWithProvider(user, tokenProvider);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.type, event.type); expect(res, isSameUserAs(user));
expect(res.connectionId, event.connectionId);
expect(res.me, isSameUserAs(user));
}); });
group('`.connectGuestUser`', () { group('`.connectGuestUser`', () {
@@ -127,9 +123,7 @@ void main() {
final res = await client.connectGuestUser(user); final res = await client.connectGuestUser(user);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.type, event.type); expect(res, isSameUserAs(user));
expect(res.connectionId, event.connectionId);
expect(res.me, isSameUserAs(user));
verify( verify(
() => api.guest.getGuestUser(any(that: isSameUserAs(user))), () => api.guest.getGuestUser(any(that: isSameUserAs(user))),
@@ -175,9 +169,6 @@ void main() {
final res = await client.connectAnonymousUser(); final res = await client.connectAnonymousUser();
expect(res, isNotNull); expect(res, isNotNull);
expect(res.type, EventType.healthCheck);
expect(res.connectionId, 'fake-connection-id');
expect(res.me, isNotNull);
}); });
group('`.openConnection`', () { group('`.openConnection`', () {
@@ -330,6 +321,93 @@ void main() {
); );
}); });
group('Connect user calls with `connectWebSocket`: false', () {
const apiKey = 'test-api-key';
late final api = FakeChatApi();
late StreamChatClient client;
setUpAll(() {
// fallback values
registerFallbackValue<User>(FakeUser());
});
setUp(() {
client = StreamChatClient(apiKey, chatApi: api);
});
tearDown(() {
client.dispose();
});
test('`.connectUser` should succeed without connecting', () async {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
final res = await client.connectUser(
user,
token,
connectWebSocket: false,
);
expect(res, isSameUserAs(user));
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
});
test(
'`.connectUserWithProvider` should succeed without connecting',
() async {
final user = User(id: 'test-user-id');
Future<String> tokenProvider(String userId) async {
expect(userId, user.id);
return Token.development(userId).rawValue;
}
final res = await client.connectUserWithProvider(
user,
tokenProvider,
connectWebSocket: false,
);
expect(res, isSameUserAs(user));
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
},
);
test('`.connectGuestUser` should succeed without connecting', () async {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
when(() => api.guest.getGuestUser(any(that: isSameUserAs(user))))
.thenAnswer(
(_) async => ConnectGuestUserResponse()
..user = user
..accessToken = token,
);
final res = await client.connectGuestUser(
user,
connectWebSocket: false,
);
expect(res, isSameUserAs(user));
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
verify(
() => api.guest.getGuestUser(any(that: isSameUserAs(user))),
).called(1);
});
test(
'`.connectAnonymousUser` should succeed without connecting',
() async {
final res = await client.connectAnonymousUser(
connectWebSocket: false,
);
expect(res, isNotNull);
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
},
);
});
group('Fake web-socket connection function with failure and persistence', () { group('Fake web-socket connection function with failure and persistence', () {
const apiKey = 'test-api-key'; const apiKey = 'test-api-key';
late final api = FakeChatApi(); late final api = FakeChatApi();
@@ -366,8 +444,7 @@ void main() {
final res = await client.connectUser(user, token); final res = await client.connectUser(user, token);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id'); expect(res, isSameUserAs(user));
expect(res.me?.id, user.id);
verify(persistence.getConnectionInfo).called(1); verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence); verifyNoMoreInteractions(persistence);
@@ -391,8 +468,7 @@ void main() {
final res = await client.connectUserWithProvider(user, tokenProvider); final res = await client.connectUserWithProvider(user, tokenProvider);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id'); expect(res, isSameUserAs(user));
expect(res.me?.id, user.id);
verify(persistence.getConnectionInfo).called(1); verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence); verifyNoMoreInteractions(persistence);
@@ -420,8 +496,7 @@ void main() {
final res = await client.connectGuestUser(user); final res = await client.connectGuestUser(user);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id'); expect(res, isSameUserAs(user));
expect(res.me?.id, user.id);
verify(persistence.getConnectionInfo).called(1); verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence); verifyNoMoreInteractions(persistence);
@@ -446,8 +521,6 @@ void main() {
final res = await client.connectAnonymousUser(); final res = await client.connectAnonymousUser();
expect(res, isNotNull); expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id');
expect(res.me?.id, user.id);
verify(persistence.getConnectionInfo).called(1); verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence); verifyNoMoreInteractions(persistence);
@@ -41,5 +41,43 @@ void main() {
expect(ownUser.banned, user.banned); expect(ownUser.banned, user.banned);
expect(ownUser.extraData, user.extraData); expect(ownUser.extraData, user.extraData);
}); });
test('copyWith', () {
final user = OwnUser.fromJson(jsonFixture('own_user.json'));
var newUser = user.copyWith();
expect(newUser.id, user.id);
expect(newUser.role, user.role);
expect(newUser.name, user.name);
newUser = user.copyWith(
id: 'test',
role: 'test',
extraData: {
'name': 'test',
},
);
expect(newUser.id, 'test');
expect(newUser.role, 'test');
expect(newUser.name, 'test');
});
test('merge', () {
final user = OwnUser.fromJson(jsonFixture('own_user.json'));
final newUser = user.merge(OwnUser(
id: 'test',
role: 'test',
extraData: const {
'name': 'test',
},
banned: true,
));
expect(newUser.id, 'test');
expect(newUser.role, 'test');
expect(newUser.name, 'test');
expect(newUser.banned, true);
});
}); });
} }
@@ -204,7 +204,8 @@ void main() {
final event = Event(type: EventType.any); final event = Event(type: EventType.any);
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
when(() => mockClient.openConnection()).thenAnswer((_) async => event); when(() => mockClient.openConnection())
.thenAnswer((_) async => OwnUser(id: 'test'));
when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.closeConnection()).thenAnswer((_) async => null);
when(() => mockClient.wsConnectionStatus) when(() => mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected); .thenReturn(ConnectionStatus.disconnected);
@@ -238,7 +239,8 @@ void main() {
final event = Event(); final event = Event();
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
when(() => mockClient.openConnection()).thenAnswer((_) async => event); when(() => mockClient.openConnection())
.thenAnswer((_) async => OwnUser(id: 'test'));
when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.closeConnection()).thenAnswer((_) async => null);
when(() => mockClient.wsConnectionStatus) when(() => mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected); .thenReturn(ConnectionStatus.disconnected);
@@ -327,7 +329,8 @@ void main() {
final event = Event(); final event = Event();
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
when(() => mockClient.openConnection()).thenAnswer((_) async => event); when(() => mockClient.openConnection())
.thenAnswer((_) async => OwnUser(id: 'test'));
when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.closeConnection()).thenAnswer((_) async => null);
when(() => mockClient.wsConnectionStatus) when(() => mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected); .thenReturn(ConnectionStatus.disconnected);
@@ -376,7 +379,8 @@ void main() {
final event = Event(); final event = Event();
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
when(() => mockClient.openConnection()).thenAnswer((_) async => event); when(() => mockClient.openConnection())
.thenAnswer((_) async => OwnUser(id: 'test'));
when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.closeConnection()).thenAnswer((_) async => null);
when(() => mockClient.wsConnectionStatus) when(() => mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.connected); .thenReturn(ConnectionStatus.connected);
@@ -402,7 +406,8 @@ void main() {
final event = Event(); final event = Event();
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
when(() => mockClient.openConnection()).thenAnswer((_) async => event); when(() => mockClient.openConnection())
.thenAnswer((_) async => OwnUser(id: 'test'));
when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.closeConnection()).thenAnswer((_) async => null);
when(() => mockClient.wsConnectionStatus) when(() => mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected); .thenReturn(ConnectionStatus.disconnected);