rename package folders
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
/// Used to notify the WS connection status
|
||||
enum ConnectionStatus {
|
||||
/// WS is connected and everything is good
|
||||
connected,
|
||||
|
||||
/// WS is connecting (usually reconnecting)
|
||||
connecting,
|
||||
|
||||
/// WS is disconnected and it's not reconnecting
|
||||
disconnected,
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'requests.g.dart';
|
||||
|
||||
/// Sorting options
|
||||
@JsonSerializable(createFactory: false)
|
||||
class SortOption {
|
||||
/// Ascending order
|
||||
static const ASC = 1;
|
||||
|
||||
/// Descending order
|
||||
static const DESC = -1;
|
||||
|
||||
/// A sorting field name
|
||||
final String field;
|
||||
|
||||
/// A sorting direction
|
||||
final int direction;
|
||||
|
||||
/// Creates a new SortOption instance
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// // Sort channels by the last message date:
|
||||
/// final sorting = SortOption("last_message_at")
|
||||
/// ```
|
||||
const SortOption(this.field, {this.direction = DESC});
|
||||
|
||||
/// Serialize model to json
|
||||
Map<String, dynamic> toJson() => _$SortOptionToJson(this);
|
||||
}
|
||||
|
||||
/// Pagination options.
|
||||
@JsonSerializable(createFactory: false, includeIfNull: false)
|
||||
class PaginationParams {
|
||||
/// The amount of items requested from the APIs.
|
||||
final int limit;
|
||||
|
||||
/// The offset of requesting items.
|
||||
final int offset;
|
||||
|
||||
/// Filter on ids greater than the given value.
|
||||
@JsonKey(name: 'id_gt')
|
||||
final String greaterThan;
|
||||
|
||||
/// Filter on ids greater than or equal to the given value.
|
||||
@JsonKey(name: 'id_gte')
|
||||
final String greaterThanOrEqual;
|
||||
|
||||
/// Filter on ids smaller than the given value.
|
||||
@JsonKey(name: 'id_lt')
|
||||
final String lessThan;
|
||||
|
||||
/// Filter on ids smaller than or equal to the given value.
|
||||
@JsonKey(name: 'id_lte')
|
||||
final String lessThanOrEqual;
|
||||
|
||||
/// Creates a new PaginationParams instance
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// // limit to 50
|
||||
/// final paginationParams = PaginationParams(limit: 50);
|
||||
///
|
||||
/// // limit to 50 with offset
|
||||
/// final paginationParams = PaginationParams(limit: 50, offset: 50);
|
||||
/// ```
|
||||
const PaginationParams({
|
||||
this.limit = 10,
|
||||
this.offset,
|
||||
this.greaterThan,
|
||||
this.greaterThanOrEqual,
|
||||
this.lessThan,
|
||||
this.lessThanOrEqual,
|
||||
});
|
||||
|
||||
/// Serialize model to json
|
||||
Map<String, dynamic> toJson() => _$PaginationParamsToJson(this);
|
||||
|
||||
/// Creates a copy of [PaginationParams] with specified attributes overridden.
|
||||
PaginationParams copyWith({
|
||||
int limit,
|
||||
int offset,
|
||||
String greaterThan,
|
||||
String greaterThanOrEqual,
|
||||
String lessThan,
|
||||
String lessThanOrEqual,
|
||||
}) =>
|
||||
PaginationParams(
|
||||
limit: limit ?? this.limit,
|
||||
offset: offset ?? this.offset,
|
||||
greaterThan: greaterThan ?? this.greaterThan,
|
||||
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
|
||||
lessThan: lessThan ?? this.lessThan,
|
||||
lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'requests.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Map<String, dynamic> _$SortOptionToJson(SortOption instance) =>
|
||||
<String, dynamic>{
|
||||
'field': instance.field,
|
||||
'direction': instance.direction,
|
||||
};
|
||||
|
||||
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('limit', instance.limit);
|
||||
writeNotNull('offset', instance.offset);
|
||||
writeNotNull('id_gt', instance.greaterThan);
|
||||
writeNotNull('id_gte', instance.greaterThanOrEqual);
|
||||
writeNotNull('id_lt', instance.lessThan);
|
||||
writeNotNull('id_lte', instance.lessThanOrEqual);
|
||||
return val;
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/models/device.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
|
||||
import '../models/channel_model.dart';
|
||||
import '../models/channel_state.dart';
|
||||
import '../models/member.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/reaction.dart';
|
||||
import '../models/read.dart';
|
||||
import '../models/user.dart';
|
||||
|
||||
part 'responses.g.dart';
|
||||
|
||||
class _BaseResponse {
|
||||
String duration;
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.resync] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SyncResponse extends _BaseResponse {
|
||||
/// The list of events
|
||||
List<Event> events;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SyncResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SyncResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.queryChannels] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class QueryChannelsResponse extends _BaseResponse {
|
||||
/// List of channels state returned by the query
|
||||
List<ChannelState> channels;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static QueryChannelsResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$QueryChannelsResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.queryChannels] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class TranslateMessageResponse extends _BaseResponse {
|
||||
/// List of channels state returned by the query
|
||||
TranslatedMessage message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static TranslateMessageResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$TranslateMessageResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.queryChannels] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class QueryMembersResponse extends _BaseResponse {
|
||||
/// List of channels state returned by the query
|
||||
List<Member> members;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static QueryMembersResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$QueryMembersResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.queryUsers] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class QueryUsersResponse extends _BaseResponse {
|
||||
/// List of users returned by the query
|
||||
List<User> users;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static QueryUsersResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$QueryUsersResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [channel.getReactions] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class QueryReactionsResponse extends _BaseResponse {
|
||||
/// List of reactions returned by the query
|
||||
List<Reaction> reactions;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static QueryReactionsResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$QueryReactionsResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.getReplies] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class QueryRepliesResponse extends _BaseResponse {
|
||||
/// List of messages returned by the api call
|
||||
List<Message> messages;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static QueryRepliesResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$QueryRepliesResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.getDevices] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class ListDevicesResponse extends _BaseResponse {
|
||||
/// List of user devices
|
||||
List<Device> devices;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static ListDevicesResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$ListDevicesResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.sendFile] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SendFileResponse extends _BaseResponse {
|
||||
/// The url of the uploaded file
|
||||
String file;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SendFileResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SendFileResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.sendImage] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SendImageResponse extends _BaseResponse {
|
||||
/// The url of the uploaded file
|
||||
String file;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SendImageResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SendImageResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.sendReaction] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SendReactionResponse extends _BaseResponse {
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// The reaction created by the api call
|
||||
Reaction reaction;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SendReactionResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SendReactionResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.setGuestUser] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SetGuestUserResponse extends _BaseResponse {
|
||||
/// Guest user access token
|
||||
String accessToken;
|
||||
|
||||
/// Guest user
|
||||
User user;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SetGuestUserResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SetGuestUserResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.updateUser] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class UpdateUsersResponse extends _BaseResponse {
|
||||
/// Updated users
|
||||
Map<String, User> users;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static UpdateUsersResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateUsersResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.updateMessage] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class UpdateMessageResponse extends _BaseResponse {
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static UpdateMessageResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateMessageResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.sendMessage] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SendMessageResponse extends _BaseResponse {
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SendMessageResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SendMessageResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.getMessage] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class GetMessageResponse extends _BaseResponse {
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Channel of the message
|
||||
ChannelModel channel;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static GetMessageResponse fromJson(Map<String, dynamic> json) {
|
||||
final res = _$GetMessageResponseFromJson(json);
|
||||
final jsonChannel = res.message?.extraData?.remove('channel');
|
||||
if (jsonChannel != null) {
|
||||
res.channel = ChannelModel.fromJson(jsonChannel);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.search] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SearchMessagesResponse extends _BaseResponse {
|
||||
/// List of messages returned by the api call
|
||||
List<GetMessageResponse> results;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SearchMessagesResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SearchMessagesResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.getMessagesById] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class GetMessagesByIdResponse extends _BaseResponse {
|
||||
/// Message returned by the api call
|
||||
List<Message> messages;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static GetMessagesByIdResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$GetMessagesByIdResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.update] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class UpdateChannelResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static UpdateChannelResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateChannelResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.inviteMembers] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class InviteMembersResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static InviteMembersResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$InviteMembersResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.removeMembers] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class RemoveMembersResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static RemoveMembersResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$RemoveMembersResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.sendAction] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SendActionResponse extends _BaseResponse {
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static SendActionResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$SendActionResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.addMembers] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class AddMembersResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static AddMembersResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$AddMembersResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.acceptInvite] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class AcceptInviteResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static AcceptInviteResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$AcceptInviteResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.rejectInvite] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class RejectInviteResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Message returned by the api call
|
||||
Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static RejectInviteResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$RejectInviteResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for empty responses
|
||||
@JsonSerializable(createToJson: false)
|
||||
class EmptyResponse extends _BaseResponse {
|
||||
/// Create a new instance from a json
|
||||
static EmptyResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$EmptyResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Model response for [Channel.query] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class ChannelStateResponse extends _BaseResponse {
|
||||
/// Updated channel
|
||||
ChannelModel channel;
|
||||
|
||||
/// List of messages returned by the api call
|
||||
List<Message> messages;
|
||||
|
||||
/// Channel members
|
||||
List<Member> members;
|
||||
|
||||
/// Number of users watching the channel
|
||||
int watcherCount;
|
||||
|
||||
/// List of read states
|
||||
List<Read> read;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static ChannelStateResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelStateResponseFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'responses.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SyncResponse _$SyncResponseFromJson(Map json) {
|
||||
return SyncResponse()
|
||||
..duration = json['duration'] as String
|
||||
..events = (json['events'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Event.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
QueryChannelsResponse _$QueryChannelsResponseFromJson(Map json) {
|
||||
return QueryChannelsResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channels = (json['channels'] as List)
|
||||
?.map((e) => e == null ? null : ChannelState.fromJson(e as Map))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
TranslateMessageResponse _$TranslateMessageResponseFromJson(Map json) {
|
||||
return TranslateMessageResponse()
|
||||
..duration = json['duration'] as String
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: TranslatedMessage.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
QueryMembersResponse _$QueryMembersResponseFromJson(Map json) {
|
||||
return QueryMembersResponse()
|
||||
..duration = json['duration'] as String
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
QueryUsersResponse _$QueryUsersResponseFromJson(Map json) {
|
||||
return QueryUsersResponse()
|
||||
..duration = json['duration'] as String
|
||||
..users = (json['users'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: User.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
QueryReactionsResponse _$QueryReactionsResponseFromJson(Map json) {
|
||||
return QueryReactionsResponse()
|
||||
..duration = json['duration'] as String
|
||||
..reactions = (json['reactions'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Reaction.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
QueryRepliesResponse _$QueryRepliesResponseFromJson(Map json) {
|
||||
return QueryRepliesResponse()
|
||||
..duration = json['duration'] as String
|
||||
..messages = (json['messages'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Message.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
ListDevicesResponse _$ListDevicesResponseFromJson(Map json) {
|
||||
return ListDevicesResponse()
|
||||
..duration = json['duration'] as String
|
||||
..devices = (json['devices'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Device.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
SendFileResponse _$SendFileResponseFromJson(Map json) {
|
||||
return SendFileResponse()
|
||||
..duration = json['duration'] as String
|
||||
..file = json['file'] as String;
|
||||
}
|
||||
|
||||
SendImageResponse _$SendImageResponseFromJson(Map json) {
|
||||
return SendImageResponse()
|
||||
..duration = json['duration'] as String
|
||||
..file = json['file'] as String;
|
||||
}
|
||||
|
||||
SendReactionResponse _$SendReactionResponseFromJson(Map json) {
|
||||
return SendReactionResponse()
|
||||
..duration = json['duration'] as String
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..reaction = json['reaction'] == null
|
||||
? null
|
||||
: Reaction.fromJson((json['reaction'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
SetGuestUserResponse _$SetGuestUserResponseFromJson(Map json) {
|
||||
return SetGuestUserResponse()
|
||||
..duration = json['duration'] as String
|
||||
..accessToken = json['access_token'] as String
|
||||
..user = json['user'] == null
|
||||
? null
|
||||
: User.fromJson((json['user'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) {
|
||||
return UpdateUsersResponse()
|
||||
..duration = json['duration'] as String
|
||||
..users = (json['users'] as Map)?.map(
|
||||
(k, e) => MapEntry(
|
||||
k as String,
|
||||
e == null
|
||||
? null
|
||||
: User.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
UpdateMessageResponse _$UpdateMessageResponseFromJson(Map json) {
|
||||
return UpdateMessageResponse()
|
||||
..duration = json['duration'] as String
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
SendMessageResponse _$SendMessageResponseFromJson(Map json) {
|
||||
return SendMessageResponse()
|
||||
..duration = json['duration'] as String
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
GetMessageResponse _$GetMessageResponseFromJson(Map json) {
|
||||
return GetMessageResponse()
|
||||
..duration = json['duration'] as String
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
SearchMessagesResponse _$SearchMessagesResponseFromJson(Map json) {
|
||||
return SearchMessagesResponse()
|
||||
..duration = json['duration'] as String
|
||||
..results = (json['results'] as List)
|
||||
?.map((e) => e == null ? null : GetMessageResponse.fromJson(e as Map))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(Map json) {
|
||||
return GetMessagesByIdResponse()
|
||||
..duration = json['duration'] as String
|
||||
..messages = (json['messages'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Message.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
UpdateChannelResponse _$UpdateChannelResponseFromJson(Map json) {
|
||||
return UpdateChannelResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
InviteMembersResponse _$InviteMembersResponseFromJson(Map json) {
|
||||
return InviteMembersResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
RemoveMembersResponse _$RemoveMembersResponseFromJson(Map json) {
|
||||
return RemoveMembersResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
SendActionResponse _$SendActionResponseFromJson(Map json) {
|
||||
return SendActionResponse()
|
||||
..duration = json['duration'] as String
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
AddMembersResponse _$AddMembersResponseFromJson(Map json) {
|
||||
return AddMembersResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
AcceptInviteResponse _$AcceptInviteResponseFromJson(Map json) {
|
||||
return AcceptInviteResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
RejectInviteResponse _$RejectInviteResponseFromJson(Map json) {
|
||||
return RejectInviteResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..message = json['message'] == null
|
||||
? null
|
||||
: Message.fromJson((json['message'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
));
|
||||
}
|
||||
|
||||
EmptyResponse _$EmptyResponseFromJson(Map json) {
|
||||
return EmptyResponse()..duration = json['duration'] as String;
|
||||
}
|
||||
|
||||
ChannelStateResponse _$ChannelStateResponseFromJson(Map json) {
|
||||
return ChannelStateResponse()
|
||||
..duration = json['duration'] as String
|
||||
..channel = json['channel'] == null
|
||||
? null
|
||||
: ChannelModel.fromJson((json['channel'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
))
|
||||
..messages = (json['messages'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Message.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..members = (json['members'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Member.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList()
|
||||
..watcherCount = json['watcher_count'] as int
|
||||
..read = (json['read'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: Read.fromJson((e as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)))
|
||||
?.toList();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/exceptions.dart';
|
||||
|
||||
/// The retry options
|
||||
class RetryPolicy {
|
||||
/// Instantiate a new RetryPolicy
|
||||
RetryPolicy({
|
||||
@required this.shouldRetry,
|
||||
@required this.retryTimeout,
|
||||
this.attempt,
|
||||
});
|
||||
|
||||
/// The number of attempts tried so far
|
||||
int attempt = 0;
|
||||
|
||||
/// This function evaluates if we should retry the failure
|
||||
final bool Function(StreamChatClient client, int attempt, ApiError apiError)
|
||||
shouldRetry;
|
||||
|
||||
/// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout
|
||||
final Duration Function(
|
||||
StreamChatClient client, int attempt, ApiError apiError) retryTimeout;
|
||||
|
||||
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
|
||||
RetryPolicy copyWith({
|
||||
bool Function(StreamChatClient client, int attempt, ApiError apiError)
|
||||
shouldRetry,
|
||||
Duration Function(StreamChatClient client, int attempt, ApiError apiError)
|
||||
retryTimeout,
|
||||
int attempt,
|
||||
}) =>
|
||||
RetryPolicy(
|
||||
retryTimeout: retryTimeout ?? this.retryTimeout,
|
||||
shouldRetry: shouldRetry ?? this.shouldRetry,
|
||||
attempt: attempt ?? this.attempt,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:stream_chat/src/api/channel.dart';
|
||||
import 'package:stream_chat/src/api/retry_policy.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/exceptions.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
/// The retry queue associated to a channel
|
||||
class RetryQueue {
|
||||
/// The channel of this queue
|
||||
final Channel channel;
|
||||
|
||||
/// The logger associated to this queue
|
||||
final Logger logger;
|
||||
|
||||
/// Instantiate a new RetryQueue object
|
||||
RetryQueue({
|
||||
@required this.channel,
|
||||
this.logger,
|
||||
}) {
|
||||
_retryPolicy = channel.client.retryPolicy;
|
||||
|
||||
_listenConnectionRecovered();
|
||||
|
||||
_listenFailedEvents();
|
||||
}
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
void _listenConnectionRecovered() {
|
||||
_subscriptions
|
||||
.add(channel.client.on(EventType.connectionRecovered).listen((event) {
|
||||
if (!_isRetrying && event.online) {
|
||||
_startRetrying();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
final HeapPriorityQueue<Message> _messageQueue = HeapPriorityQueue(_byDate);
|
||||
bool _isRetrying = false;
|
||||
RetryPolicy _retryPolicy;
|
||||
|
||||
/// Add a list of messages
|
||||
void add(List<Message> messages) {
|
||||
final messageList = _messageQueue.toList();
|
||||
_messageQueue.addAll(messages
|
||||
.where((element) => !messageList.any((m) => m.id == element.id)));
|
||||
|
||||
if (_messageQueue.isNotEmpty && !_isRetrying) {
|
||||
_startRetrying();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startRetrying() async {
|
||||
logger?.info('start retrying');
|
||||
_isRetrying = true;
|
||||
final retryPolicy = _retryPolicy.copyWith(attempt: 0);
|
||||
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.first;
|
||||
try {
|
||||
logger?.info('retry attempt ${retryPolicy.attempt}');
|
||||
await _sendMessage(message);
|
||||
logger?.info('message sent - removing it from the queue');
|
||||
_messageQueue.remove(message);
|
||||
logger?.info('now ${_messageQueue.length} messages in the queue');
|
||||
retryPolicy.attempt = 0;
|
||||
} catch (error) {
|
||||
ApiError apiError;
|
||||
if (error is DioError) {
|
||||
if (error.type == DioErrorType.RESPONSE) {
|
||||
_messageQueue.remove(message);
|
||||
return;
|
||||
}
|
||||
apiError = ApiError(
|
||||
error.response?.data,
|
||||
error.response?.statusCode,
|
||||
);
|
||||
} else if (error is ApiError) {
|
||||
apiError = error;
|
||||
if (apiError.status?.toString()?.startsWith('4') == true) {
|
||||
_messageQueue.remove(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!retryPolicy.shouldRetry(
|
||||
channel.client,
|
||||
retryPolicy.attempt,
|
||||
apiError,
|
||||
)) {
|
||||
_messageQueue.toList().forEach(_sendFailedEvent);
|
||||
_isRetrying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
retryPolicy.attempt++;
|
||||
final timeout = retryPolicy.retryTimeout(
|
||||
channel.client,
|
||||
retryPolicy.attempt,
|
||||
apiError,
|
||||
);
|
||||
await Future.delayed(timeout);
|
||||
}
|
||||
}
|
||||
_isRetrying = false;
|
||||
}
|
||||
|
||||
void _sendFailedEvent(Message message) {
|
||||
final newStatus = message.status == MessageSendingStatus.sending
|
||||
? MessageSendingStatus.failed
|
||||
: (message.status == MessageSendingStatus.updating
|
||||
? MessageSendingStatus.failed_update
|
||||
: MessageSendingStatus.failed_delete);
|
||||
channel.state.addMessage(message.copyWith(
|
||||
status: newStatus,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _sendMessage(Message message) async {
|
||||
if (message.status == MessageSendingStatus.failed_update ||
|
||||
message.status == MessageSendingStatus.updating) {
|
||||
await channel.client.updateMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
} else if (message.status == MessageSendingStatus.failed ||
|
||||
message.status == MessageSendingStatus.sending) {
|
||||
await channel.sendMessage(
|
||||
message,
|
||||
);
|
||||
} else if (message.status == MessageSendingStatus.failed_delete ||
|
||||
message.status == MessageSendingStatus.deleting) {
|
||||
await channel.client.deleteMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _listenFailedEvents() {
|
||||
_subscriptions.add(channel.on().listen((event) {
|
||||
final messageList = _messageQueue.toList();
|
||||
if (event.message != null) {
|
||||
final messageIndex =
|
||||
messageList.indexWhere((m) => m.id == event.message.id);
|
||||
if (messageIndex == -1 &&
|
||||
[
|
||||
MessageSendingStatus.failed_update,
|
||||
MessageSendingStatus.failed,
|
||||
MessageSendingStatus.failed_delete,
|
||||
].contains(event.message.status)) {
|
||||
logger?.info('add message from events');
|
||||
add([event.message]);
|
||||
} else if (messageIndex != -1 &&
|
||||
[
|
||||
MessageSendingStatus.sent,
|
||||
null,
|
||||
].contains(event.message.status)) {
|
||||
_messageQueue.remove(messageList[messageIndex]);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Call this method to dispose this object
|
||||
void dispose() {
|
||||
_messageQueue.clear();
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
}
|
||||
|
||||
static int _byDate(Message m1, Message m2) {
|
||||
final date1 = _getMessageDate(m1);
|
||||
final date2 = _getMessageDate(m2);
|
||||
|
||||
return date1.compareTo(date2);
|
||||
}
|
||||
|
||||
static DateTime _getMessageDate(Message m1) {
|
||||
switch (m1.status) {
|
||||
case MessageSendingStatus.failed_delete:
|
||||
case MessageSendingStatus.deleting:
|
||||
return m1.deletedAt;
|
||||
|
||||
case MessageSendingStatus.failed:
|
||||
case MessageSendingStatus.sending:
|
||||
return m1.createdAt;
|
||||
|
||||
case MessageSendingStatus.failed_update:
|
||||
case MessageSendingStatus.updating:
|
||||
return m1.updatedAt;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:web_socket_channel/html.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// Html version of websocket implementation
|
||||
/// Used in Flutter web version
|
||||
WebSocketChannel connectWebSocket(String url, {Iterable<String> protocols}) =>
|
||||
HtmlWebSocketChannel.connect(url, protocols: protocols);
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// IO version of websocket implementation
|
||||
/// Used in Flutter mobile version
|
||||
WebSocketChannel connectWebSocket(String url, {Iterable<String> protocols}) =>
|
||||
IOWebSocketChannel.connect(url, protocols: protocols);
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// Stub version of websocket implementation
|
||||
/// Used just for conditional library import
|
||||
WebSocketChannel connectWebSocket(String url,
|
||||
{Iterable<String> protocols,
|
||||
Map<String, dynamic> headers,
|
||||
Duration pingInterval}) =>
|
||||
throw UnimplementedError();
|
||||
@@ -0,0 +1,316 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../models/event.dart';
|
||||
import '../models/user.dart';
|
||||
import 'connection_status.dart';
|
||||
import 'web_socket_channel_stub.dart'
|
||||
if (dart.library.html) 'web_socket_channel_html.dart'
|
||||
if (dart.library.io) 'web_socket_channel_io.dart';
|
||||
|
||||
/// Typedef which exposes an [Event] as the only parameter.
|
||||
typedef EventHandler = void Function(Event);
|
||||
|
||||
/// Typedef used for connecting to a websocket. Method returns a [WebSocketChannel]
|
||||
/// and accepts a connection [url] and an optional [Iterable] of `protocols`.
|
||||
typedef ConnectWebSocket = WebSocketChannel Function(String url,
|
||||
{Iterable<String> protocols});
|
||||
|
||||
// TODO: parse error even
|
||||
// TODO: if parsing an error into an event fails we should not hide the original error
|
||||
/// A WebSocket connection that reconnects upon failure.
|
||||
class WebSocket {
|
||||
/// Creates a new websocket
|
||||
/// To connect the WS call [connect]
|
||||
WebSocket({
|
||||
@required this.baseUrl,
|
||||
this.user,
|
||||
this.connectParams,
|
||||
this.connectPayload,
|
||||
this.handler,
|
||||
this.logger,
|
||||
this.connectFunc = connectWebSocket,
|
||||
this.reconnectionMonitorInterval = 1,
|
||||
this.healthCheckInterval = 20,
|
||||
this.reconnectionMonitorTimeout = 40,
|
||||
}) {
|
||||
final qs = Map<String, String>.from(connectParams);
|
||||
|
||||
final data = Map<String, dynamic>.from(connectPayload);
|
||||
|
||||
data['user_details'] = user.toJson();
|
||||
qs['json'] = json.encode(data);
|
||||
|
||||
if (baseUrl.startsWith('https')) {
|
||||
_path = baseUrl.replaceFirst('https://', '');
|
||||
_path = Uri.https(_path, 'connect', qs)
|
||||
.toString()
|
||||
.replaceFirst('https', 'wss');
|
||||
} else if (baseUrl.startsWith('http')) {
|
||||
_path = baseUrl.replaceFirst('http://', '');
|
||||
_path =
|
||||
Uri.http(_path, 'connect', qs).toString().replaceFirst('http', 'ws');
|
||||
} else {
|
||||
_path = Uri.https(baseUrl, 'connect', qs)
|
||||
.toString()
|
||||
.replaceFirst('https', 'wss');
|
||||
}
|
||||
}
|
||||
|
||||
/// WS base url
|
||||
final String baseUrl;
|
||||
|
||||
/// User performing the WS connection
|
||||
final User user;
|
||||
|
||||
/// Querystring connection parameters
|
||||
final Map<String, String> connectParams;
|
||||
|
||||
/// WS connection payload
|
||||
final Map<String, dynamic> connectPayload;
|
||||
|
||||
/// Functions that will be called every time a new event is received from the connection
|
||||
final EventHandler handler;
|
||||
|
||||
/// A WS specific logger instance
|
||||
final Logger logger;
|
||||
|
||||
/// Connection function
|
||||
/// Used only for testing purpose
|
||||
@visibleForTesting
|
||||
final ConnectWebSocket connectFunc;
|
||||
|
||||
/// Interval of the reconnection monitor timer
|
||||
/// This checks that it received a new event in the last [reconnectionMonitorTimeout] seconds,
|
||||
/// otherwise it considers the connection unhealthy and reconnects the WS
|
||||
final int reconnectionMonitorInterval;
|
||||
|
||||
/// Interval of the health event sending timer
|
||||
/// This sends a health event every [healthCheckInterval] seconds in order to
|
||||
/// make the server aware that the client is still listening
|
||||
final int healthCheckInterval;
|
||||
|
||||
/// The timeout that uses the reconnection monitor timer to consider the connection unhealthy
|
||||
final int reconnectionMonitorTimeout;
|
||||
|
||||
final _connectionStatusController =
|
||||
BehaviorSubject.seeded(ConnectionStatus.disconnected);
|
||||
|
||||
set _connectionStatus(ConnectionStatus status) =>
|
||||
_connectionStatusController.add(status);
|
||||
|
||||
/// The current connection status value
|
||||
ConnectionStatus get connectionStatus => _connectionStatusController.value;
|
||||
|
||||
/// This notifies of connection status changes
|
||||
Stream<ConnectionStatus> get connectionStatusStream =>
|
||||
_connectionStatusController.stream;
|
||||
|
||||
String _path;
|
||||
int _retryAttempt = 1;
|
||||
WebSocketChannel _channel;
|
||||
Timer _healthCheck, _reconnectionMonitor;
|
||||
DateTime _lastEventAt;
|
||||
bool _manuallyDisconnected = false,
|
||||
_connecting = false,
|
||||
_reconnecting = false;
|
||||
|
||||
Event _decodeEvent(String source) {
|
||||
return Event.fromJson(json.decode(source));
|
||||
}
|
||||
|
||||
Completer<Event> _connectionCompleter = Completer<Event>();
|
||||
|
||||
/// Connect the WS using the parameters passed in the constructor
|
||||
Future<Event> connect() {
|
||||
_manuallyDisconnected = false;
|
||||
|
||||
if (_connecting) {
|
||||
logger.severe('already connecting');
|
||||
return null;
|
||||
}
|
||||
|
||||
_connecting = true;
|
||||
_connectionStatus = ConnectionStatus.connecting;
|
||||
|
||||
logger.info('connecting to $_path');
|
||||
|
||||
_channel = connectFunc(_path);
|
||||
_channel.stream.listen(
|
||||
(data) {
|
||||
final jsonData = json.decode(data);
|
||||
if (jsonData['error'] != null) {
|
||||
return _onConnectionError(jsonData['error']);
|
||||
}
|
||||
_onData(data);
|
||||
},
|
||||
onError: (error, stacktrace) {
|
||||
_onConnectionError(error, stacktrace);
|
||||
},
|
||||
onDone: () {
|
||||
_onDone();
|
||||
},
|
||||
);
|
||||
return _connectionCompleter.future;
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
_connecting = false;
|
||||
if (_manuallyDisconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'connection closed | closeCode: ${_channel.closeCode} | closedReason: ${_channel.closeReason}');
|
||||
|
||||
if (!_reconnecting) {
|
||||
_reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _onData(data) {
|
||||
final event = _decodeEvent(data);
|
||||
logger.info('received new event: $data');
|
||||
|
||||
if (_lastEventAt == null) {
|
||||
logger.info('connection estabilished');
|
||||
_connecting = false;
|
||||
_reconnecting = false;
|
||||
_lastEventAt = DateTime.now();
|
||||
|
||||
_connectionStatus = ConnectionStatus.connected;
|
||||
_retryAttempt = 1;
|
||||
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
_connectionCompleter.complete(event);
|
||||
}
|
||||
|
||||
_startReconnectionMonitor();
|
||||
_startHealthCheck();
|
||||
}
|
||||
|
||||
handler(event);
|
||||
_lastEventAt = DateTime.now();
|
||||
}
|
||||
|
||||
Future<void> _onConnectionError(error, [stacktrace]) async {
|
||||
logger.severe('error connecting');
|
||||
logger.severe(error);
|
||||
if (stacktrace != null) {
|
||||
logger.severe(stacktrace);
|
||||
}
|
||||
_connecting = false;
|
||||
|
||||
if (!_reconnecting) {
|
||||
_connectionStatus = ConnectionStatus.disconnected;
|
||||
}
|
||||
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
_cancelTimers();
|
||||
_connectionCompleter.completeError(error, stacktrace);
|
||||
} else if (!_reconnecting) {
|
||||
return _reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _startReconnectionMonitor() {
|
||||
final reconnectionTimer = (_) {
|
||||
final now = DateTime.now();
|
||||
if (_lastEventAt != null &&
|
||||
now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) {
|
||||
_channel.sink.close();
|
||||
}
|
||||
};
|
||||
|
||||
_reconnectionMonitor = Timer.periodic(
|
||||
Duration(seconds: reconnectionMonitorInterval),
|
||||
reconnectionTimer,
|
||||
);
|
||||
|
||||
reconnectionTimer(_reconnectionMonitor);
|
||||
}
|
||||
|
||||
void _reconnectTimer() async {
|
||||
if (!_reconnecting) {
|
||||
return;
|
||||
}
|
||||
if (_connecting) {
|
||||
logger.info('already connecting');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('reconnecting..');
|
||||
|
||||
_cancelTimers();
|
||||
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
logger.log(Level.SEVERE, e.toString());
|
||||
}
|
||||
await Future.delayed(
|
||||
Duration(seconds: min(_retryAttempt * 5, 25)),
|
||||
() {
|
||||
_reconnectTimer();
|
||||
_retryAttempt++;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reconnect() async {
|
||||
logger.info('reconnect');
|
||||
if (!_reconnecting) {
|
||||
_reconnecting = true;
|
||||
_connectionStatus = ConnectionStatus.connecting;
|
||||
}
|
||||
|
||||
_reconnectTimer();
|
||||
}
|
||||
|
||||
void _cancelTimers() {
|
||||
_lastEventAt = null;
|
||||
if (_healthCheck != null) {
|
||||
_healthCheck.cancel();
|
||||
}
|
||||
if (_reconnectionMonitor != null) {
|
||||
_reconnectionMonitor.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void _startHealthCheck() {
|
||||
logger.info('start health check monitor');
|
||||
|
||||
final healthCheckTimer = (_) {
|
||||
logger.info('sending health.check');
|
||||
_channel.sink.add("{'type': 'health.check'}");
|
||||
};
|
||||
|
||||
_healthCheck = Timer.periodic(
|
||||
Duration(seconds: healthCheckInterval),
|
||||
healthCheckTimer,
|
||||
);
|
||||
|
||||
healthCheckTimer(_healthCheck);
|
||||
}
|
||||
|
||||
/// Disconnects the WS and releases eventual resources
|
||||
Future<void> disconnect() async {
|
||||
if (_manuallyDisconnected) {
|
||||
return;
|
||||
}
|
||||
logger.info('disconnecting');
|
||||
_connectionCompleter = Completer();
|
||||
_cancelTimers();
|
||||
_reconnecting = false;
|
||||
_manuallyDisconnected = true;
|
||||
_connectionStatus = ConnectionStatus.disconnected;
|
||||
await _connectionStatusController.close();
|
||||
return _channel.sink.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user