Skip to content

Commit 7d59982

Browse files
tddang-linagorahoangdat
authored andcommitted
TF-3157 Update web socket with background service worker
TF-3157 Stub BroadcastChannel for mobile build
1 parent c4d0c27 commit 7d59982

File tree

16 files changed

+414
-67
lines changed

16 files changed

+414
-67
lines changed

core/lib/data/constants/constant.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ class Constant {
77
static const octetStreamMimeType = 'application/octet-stream';
88
static const pdfExtension = '.pdf';
99
static const imageType = 'image';
10+
static const wsServiceWorkerBroadcastChannel = 'background-message';
1011
}

fcm/lib/model/type_name.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import 'package:equatable/equatable.dart';
33

44
class TypeName with EquatableMixin {
5-
static final mailboxType = TypeName('Mailbox');
6-
static final emailType = TypeName('Email');
7-
static final emailDelivery = TypeName('EmailDelivery');
5+
static const mailboxType = TypeName('Mailbox');
6+
static const emailType = TypeName('Email');
7+
static const emailDelivery = TypeName('EmailDelivery');
88

99
final String value;
1010

11-
TypeName(this.value);
11+
const TypeName(this.value);
1212

1313
@override
1414
List<Object?> get props => [value];
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
import 'dart:convert';
21

2+
import 'package:core/data/constants/constant.dart';
33
import 'package:core/utils/app_logger.dart';
4+
import 'package:core/utils/broadcast_channel/broadcast_channel.dart';
45
import 'package:jmap_dart_client/jmap/account_id.dart';
56
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
67
import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart';
78
import 'package:jmap_dart_client/jmap/core/session/session.dart';
89
import 'package:model/extensions/session_extension.dart';
910
import 'package:rxdart/transformers.dart';
1011
import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart';
12+
import 'package:tmail_ui_user/features/push_notification/data/model/connect_web_socket_message.dart';
1113
import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart';
1214
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart';
15+
import 'package:tmail_ui_user/features/push_notification/domain/model/web_socket_action.dart';
1316
import 'package:tmail_ui_user/main/error/capability_validator.dart';
1417
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
15-
import 'package:web_socket_channel/web_socket_channel.dart';
18+
import 'package:universal_html/html.dart';
1619

17-
class RemoteWebSocketDatasourceImpl implements WebSocketDatasource {
20+
class WebSocketDatasourceImpl implements WebSocketDatasource {
1821
final WebSocketApi _webSocketApi;
1922
final ExceptionThrower _exceptionThrower;
2023

21-
const RemoteWebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower);
24+
const WebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower);
25+
26+
static const String _webSocketClosed = 'webSocketClosed';
2227

2328
@override
2429
Stream getWebSocketChannel(Session session, AccountId accountId) {
@@ -30,32 +35,31 @@ class RemoteWebSocketDatasourceImpl implements WebSocketDatasource {
3035
Stream _getWebSocketChannel(
3136
Session session,
3237
AccountId accountId,
33-
[int retryRemained = 3]
34-
) async* {
38+
) async* {
39+
final broadcastChannel = BroadcastChannel(Constant.wsServiceWorkerBroadcastChannel);
3540
try {
3641
_verifyWebSocketCapabilities(session, accountId);
3742
final webSocketTicket = await _webSocketApi.getWebSocketTicket(session, accountId);
3843
final webSocketUri = _getWebSocketUri(session, accountId);
44+
window.navigator.serviceWorker?.controller?.postMessage(ConnectWebSocketMessage(
45+
webSocketAction: WebSocketAction.connect,
46+
webSocketUrl: webSocketUri.toString(),
47+
webSocketTicket: webSocketTicket
48+
).toJson());
3949

40-
final webSocketChannel = WebSocketChannel.connect(
41-
Uri.parse('$webSocketUri?ticket=$webSocketTicket'));
42-
await webSocketChannel.ready;
43-
webSocketChannel.sink.add(jsonEncode({"@type": "WebSocketPushEnable"}));
44-
45-
yield* webSocketChannel.stream;
50+
yield* _webSocketListener(broadcastChannel);
4651
} catch (e) {
4752
logError('RemoteWebSocketDatasourceImpl::getWebSocketChannel():error: $e');
48-
if (retryRemained > 0) {
49-
yield* _getWebSocketChannel(session, accountId, retryRemained - 1);
50-
} else {
51-
rethrow;
52-
}
53+
rethrow;
5354
}
5455
}
5556

5657
void _verifyWebSocketCapabilities(Session session, AccountId accountId) {
5758
if (!CapabilityIdentifier.jmapWebSocket.isSupported(session, accountId)
5859
|| !CapabilityIdentifier.jmapWebSocketTicket.isSupported(session, accountId)
60+
|| session.getCapabilityProperties<WebSocketCapability>(
61+
accountId,
62+
CapabilityIdentifier.jmapWebSocket)?.supportsPush != true
5963
) {
6064
throw WebSocketPushNotSupportedException();
6165
}
@@ -73,4 +77,14 @@ class RemoteWebSocketDatasourceImpl implements WebSocketDatasource {
7377

7478
return webSocketUri;
7579
}
80+
81+
Stream _webSocketListener(BroadcastChannel broadcastChannel) {
82+
return broadcastChannel.onMessage.map((event) {
83+
if (event.data == _webSocketClosed) {
84+
throw WebSocketClosedException();
85+
}
86+
87+
return event.data;
88+
});
89+
}
7690
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:json_annotation/json_annotation.dart';
3+
import 'package:tmail_ui_user/features/push_notification/domain/model/web_socket_action.dart';
4+
5+
part 'connect_web_socket_message.g.dart';
6+
7+
@JsonSerializable()
8+
class ConnectWebSocketMessage with EquatableMixin {
9+
@JsonKey(name: 'action')
10+
final WebSocketAction webSocketAction;
11+
@JsonKey(name: 'url')
12+
final String webSocketUrl;
13+
@JsonKey(name: 'ticket')
14+
final String webSocketTicket;
15+
16+
ConnectWebSocketMessage({
17+
required this.webSocketAction,
18+
required this.webSocketUrl,
19+
required this.webSocketTicket,
20+
});
21+
22+
factory ConnectWebSocketMessage.fromJson(Map<String, dynamic> json)
23+
=> _$ConnectWebSocketMessageFromJson(json);
24+
Map<String, dynamic> toJson() => _$ConnectWebSocketMessageToJson(this);
25+
26+
@override
27+
List<Object?> get props => [webSocketAction, webSocketUrl, webSocketTicket];
28+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
part 'web_socket_echo.g.dart';
4+
5+
@JsonSerializable(includeIfNull: false)
6+
class WebSocketEcho {
7+
@JsonKey(name: '@type')
8+
final String? type;
9+
final String? requestId;
10+
final List<List<dynamic>>? methodResponses;
11+
12+
WebSocketEcho({
13+
this.type,
14+
this.requestId,
15+
this.methodResponses,
16+
});
17+
18+
factory WebSocketEcho.fromJson(Map<String, dynamic> json) => _$WebSocketEchoFromJson(json);
19+
20+
Map<String, dynamic> toJson() => _$WebSocketEchoToJson(this);
21+
22+
static bool isValid(Map<String, dynamic> json) {
23+
try {
24+
final webSocketEcho = WebSocketEcho.fromJson(json);
25+
final listResponses = webSocketEcho.methodResponses?.firstOrNull;
26+
return listResponses?.contains('Core/echo') ?? false;
27+
} catch (_) {
28+
return false;
29+
}
30+
}
31+
}

lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ class WebSocketPushNotSupportedException implements Exception {}
22

33
class WebSocketUriUnavailableException implements Exception {}
44

5-
class WebSocketTicketUnavailableException implements Exception {}
5+
class WebSocketTicketUnavailableException implements Exception {}
6+
7+
class WebSocketClosedException implements Exception {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enum WebSocketAction {connect, disconnect}

lib/features/push_notification/domain/state/web_socket_push_state.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import 'package:jmap_dart_client/jmap/push/state_change.dart';
55
class InitializingWebSocketPushChannel extends LoadingState {}
66

77
class WebSocketPushStateReceived extends UIState {
8-
final StateChange stateChange;
8+
final StateChange? stateChange;
99

1010
WebSocketPushStateReceived(this.stateChange);
1111

1212
@override
13-
List<Object> get props => [stateChange];
13+
List<Object?> get props => [stateChange];
1414
}
1515

1616
class WebSocketConnectionFailed extends FeatureFailure {

lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart';
77
import 'package:jmap_dart_client/jmap/account_id.dart';
88
import 'package:jmap_dart_client/jmap/core/session/session.dart';
99
import 'package:jmap_dart_client/jmap/push/state_change.dart';
10+
import 'package:tmail_ui_user/features/push_notification/data/model/web_socket_echo.dart';
1011
import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart';
1112
import 'package:tmail_ui_user/features/push_notification/domain/state/web_socket_push_state.dart';
1213

@@ -31,9 +32,21 @@ class ConnectWebSocketInteractor {
3132
}
3233

3334
Either<Failure, Success> _toStateChange(dynamic data) {
34-
if (data is String) {
35-
data = jsonDecode(data);
35+
StateChange? possibleStateChange;
36+
try {
37+
if (data is String) {
38+
data = jsonDecode(data);
39+
}
40+
possibleStateChange = StateChange.fromJson(data);
41+
return Right(WebSocketPushStateReceived(possibleStateChange));
42+
} catch (e) {
43+
logError('ConnectWebSocketInteractor::_toStateChange: '
44+
'websocket message is not StateChange: $data');
45+
final dataIsWebSocketEcho = WebSocketEcho.isValid(data);
46+
if (dataIsWebSocketEcho) {
47+
return Right(WebSocketPushStateReceived(null));
48+
}
49+
return Left(WebSocketConnectionFailed(exception: e));
3650
}
37-
return Right(WebSocketPushStateReceived(StateChange.fromJson(data)));
3851
}
3952
}

lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'package:get/get.dart';
22
import 'package:tmail_ui_user/features/base/interactors_bindings.dart';
33
import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart';
4-
import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart';
4+
import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/web_socket_datasource_impl.dart';
55
import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart';
66
import 'package:tmail_ui_user/features/push_notification/data/repository/web_socket_repository_impl.dart';
77
import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart';
@@ -11,12 +11,12 @@ import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
1111
class WebSocketInteractorBindings extends InteractorsBindings {
1212
@override
1313
void bindingsDataSource() {
14-
Get.lazyPut<WebSocketDatasource>(() => Get.find<RemoteWebSocketDatasourceImpl>());
14+
Get.lazyPut<WebSocketDatasource>(() => Get.find<WebSocketDatasourceImpl>());
1515
}
1616

1717
@override
1818
void bindingsDataSourceImpl() {
19-
Get.lazyPut(() => RemoteWebSocketDatasourceImpl(
19+
Get.lazyPut(() => WebSocketDatasourceImpl(
2020
Get.find<WebSocketApi>(),
2121
Get.find<RemoteExceptionThrower>(),
2222
));

lib/features/push_notification/presentation/controller/push_base_controller.dart

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:collection/collection.dart';
24
import 'package:core/presentation/state/failure.dart';
35
import 'package:core/presentation/state/success.dart';
@@ -17,12 +19,12 @@ abstract class PushBaseController {
1719
Session? session;
1820
AccountId? accountId;
1921

22+
StreamSubscription<Either<Failure, Success>>? _stateStreamSubscription;
23+
2024
void consumeState(Stream<Either<Failure, Success>> newStateStream) {
21-
newStateStream.listen(
25+
_stateStreamSubscription = newStateStream.listen(
2226
_handleStateStream,
23-
onError: (error, stackTrace) {
24-
logError('PushBaseController::consumeState():onError:error: $error | stackTrace: $stackTrace');
25-
}
27+
onError: handleErrorViewState,
2628
);
2729
}
2830

@@ -34,6 +36,15 @@ abstract class PushBaseController {
3436

3537
void handleSuccessViewState(Success success);
3638

39+
void handleErrorViewState(Object error, StackTrace stackTrace) {
40+
logError('PushBaseController::handleErrorViewState():error: $error | stackTrace: $stackTrace');
41+
}
42+
43+
void cancelStateStreamSubscription() {
44+
_stateStreamSubscription?.cancel();
45+
_stateStreamSubscription = null;
46+
}
47+
3748
void initialize({AccountId? accountId, Session? session}) {
3849
this.accountId = accountId;
3950
this.session = session;
@@ -87,22 +98,20 @@ abstract class PushBaseController {
8798
{Session? session}
8899
) {
89100
final newState = jmap.State(mapTypeState[typeName.value]);
90-
if (typeName == TypeName.emailType) {
91-
if (isForeground) {
92-
return SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session);
93-
} else {
94-
return StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session);
95-
}
96-
} else if (typeName == TypeName.emailDelivery) {
97-
if (!isForeground) {
98-
return PushNotificationAction(typeName, newState, session, accountId, userName);
99-
}
100-
} else if (typeName == TypeName.mailboxType) {
101-
if (isForeground) {
102-
return SynchronizeMailboxOnForegroundAction(typeName, newState, accountId);
103-
} else {
104-
return StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName);
105-
}
101+
switch (typeName) {
102+
case TypeName.emailType:
103+
return isForeground
104+
? SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session)
105+
: StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session);
106+
case TypeName.emailDelivery:
107+
if (!isForeground) {
108+
return PushNotificationAction(typeName, newState, session, accountId, userName);
109+
}
110+
break;
111+
case TypeName.mailboxType:
112+
return isForeground
113+
? SynchronizeMailboxOnForegroundAction(typeName, newState, accountId)
114+
: StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName);
106115
}
107116
return null;
108117
}

0 commit comments

Comments
 (0)