diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index 5540b0ac1..088fed2aa 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -7,4 +7,5 @@ class Constant { static const octetStreamMimeType = 'application/octet-stream'; static const pdfExtension = '.pdf'; static const imageType = 'image'; + static const wsServiceWorkerBroadcastChannel = 'background-message'; } \ No newline at end of file diff --git a/fcm/lib/model/type_name.dart b/fcm/lib/model/type_name.dart index cae043199..89879245c 100644 --- a/fcm/lib/model/type_name.dart +++ b/fcm/lib/model/type_name.dart @@ -2,13 +2,13 @@ import 'package:equatable/equatable.dart'; class TypeName with EquatableMixin { - static final mailboxType = TypeName('Mailbox'); - static final emailType = TypeName('Email'); - static final emailDelivery = TypeName('EmailDelivery'); + static const mailboxType = TypeName('Mailbox'); + static const emailType = TypeName('Email'); + static const emailDelivery = TypeName('EmailDelivery'); final String value; - TypeName(this.value); + const TypeName(this.value); @override List get props => [value]; diff --git a/lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart b/lib/features/push_notification/data/datasource_impl/web_socket_datasource_impl.dart similarity index 63% rename from lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart rename to lib/features/push_notification/data/datasource_impl/web_socket_datasource_impl.dart index 8d9668796..1c88936e6 100644 --- a/lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart +++ b/lib/features/push_notification/data/datasource_impl/web_socket_datasource_impl.dart @@ -1,6 +1,7 @@ -import 'dart:convert'; +import 'package:core/data/constants/constant.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/broadcast_channel/broadcast_channel.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart'; @@ -8,17 +9,21 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/connect_web_socket_message.dart'; import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/model/web_socket_action.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:universal_html/html.dart'; -class RemoteWebSocketDatasourceImpl implements WebSocketDatasource { +class WebSocketDatasourceImpl implements WebSocketDatasource { final WebSocketApi _webSocketApi; final ExceptionThrower _exceptionThrower; - const RemoteWebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower); + const WebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower); + + static const String _webSocketClosed = 'webSocketClosed'; @override Stream getWebSocketChannel(Session session, AccountId accountId) { @@ -30,32 +35,31 @@ class RemoteWebSocketDatasourceImpl implements WebSocketDatasource { Stream _getWebSocketChannel( Session session, AccountId accountId, - [int retryRemained = 3] - ) async* { + ) async* { + final broadcastChannel = BroadcastChannel(Constant.wsServiceWorkerBroadcastChannel); try { _verifyWebSocketCapabilities(session, accountId); final webSocketTicket = await _webSocketApi.getWebSocketTicket(session, accountId); final webSocketUri = _getWebSocketUri(session, accountId); + window.navigator.serviceWorker?.controller?.postMessage(ConnectWebSocketMessage( + webSocketAction: WebSocketAction.connect, + webSocketUrl: webSocketUri.toString(), + webSocketTicket: webSocketTicket + ).toJson()); - final webSocketChannel = WebSocketChannel.connect( - Uri.parse('$webSocketUri?ticket=$webSocketTicket')); - await webSocketChannel.ready; - webSocketChannel.sink.add(jsonEncode({"@type": "WebSocketPushEnable"})); - - yield* webSocketChannel.stream; + yield* _webSocketListener(broadcastChannel); } catch (e) { logError('RemoteWebSocketDatasourceImpl::getWebSocketChannel():error: $e'); - if (retryRemained > 0) { - yield* _getWebSocketChannel(session, accountId, retryRemained - 1); - } else { - rethrow; - } + rethrow; } } void _verifyWebSocketCapabilities(Session session, AccountId accountId) { if (!CapabilityIdentifier.jmapWebSocket.isSupported(session, accountId) || !CapabilityIdentifier.jmapWebSocketTicket.isSupported(session, accountId) + || session.getCapabilityProperties( + accountId, + CapabilityIdentifier.jmapWebSocket)?.supportsPush != true ) { throw WebSocketPushNotSupportedException(); } @@ -73,4 +77,14 @@ class RemoteWebSocketDatasourceImpl implements WebSocketDatasource { return webSocketUri; } + + Stream _webSocketListener(BroadcastChannel broadcastChannel) { + return broadcastChannel.onMessage.map((event) { + if (event.data == _webSocketClosed) { + throw WebSocketClosedException(); + } + + return event.data; + }); + } } \ No newline at end of file diff --git a/lib/features/push_notification/data/model/connect_web_socket_message.dart b/lib/features/push_notification/data/model/connect_web_socket_message.dart new file mode 100644 index 000000000..b793ebcb9 --- /dev/null +++ b/lib/features/push_notification/data/model/connect_web_socket_message.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/model/web_socket_action.dart'; + +part 'connect_web_socket_message.g.dart'; + +@JsonSerializable() +class ConnectWebSocketMessage with EquatableMixin { + @JsonKey(name: 'action') + final WebSocketAction webSocketAction; + @JsonKey(name: 'url') + final String webSocketUrl; + @JsonKey(name: 'ticket') + final String webSocketTicket; + + ConnectWebSocketMessage({ + required this.webSocketAction, + required this.webSocketUrl, + required this.webSocketTicket, + }); + + factory ConnectWebSocketMessage.fromJson(Map json) + => _$ConnectWebSocketMessageFromJson(json); + Map toJson() => _$ConnectWebSocketMessageToJson(this); + + @override + List get props => [webSocketAction, webSocketUrl, webSocketTicket]; +} \ No newline at end of file diff --git a/lib/features/push_notification/data/model/web_socket_echo.dart b/lib/features/push_notification/data/model/web_socket_echo.dart new file mode 100644 index 000000000..624167ddf --- /dev/null +++ b/lib/features/push_notification/data/model/web_socket_echo.dart @@ -0,0 +1,31 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'web_socket_echo.g.dart'; + +@JsonSerializable(includeIfNull: false) +class WebSocketEcho { + @JsonKey(name: '@type') + final String? type; + final String? requestId; + final List>? methodResponses; + + WebSocketEcho({ + this.type, + this.requestId, + this.methodResponses, + }); + + factory WebSocketEcho.fromJson(Map json) => _$WebSocketEchoFromJson(json); + + Map toJson() => _$WebSocketEchoToJson(this); + + static bool isValid(Map json) { + try { + final webSocketEcho = WebSocketEcho.fromJson(json); + final listResponses = webSocketEcho.methodResponses?.firstOrNull; + return listResponses?.contains('Core/echo') ?? false; + } catch (_) { + return false; + } + } +} diff --git a/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart b/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart index 0f0a3c789..216b4c126 100644 --- a/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart +++ b/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart @@ -2,4 +2,6 @@ class WebSocketPushNotSupportedException implements Exception {} class WebSocketUriUnavailableException implements Exception {} -class WebSocketTicketUnavailableException implements Exception {} \ No newline at end of file +class WebSocketTicketUnavailableException implements Exception {} + +class WebSocketClosedException implements Exception {} \ No newline at end of file diff --git a/lib/features/push_notification/domain/model/web_socket_action.dart b/lib/features/push_notification/domain/model/web_socket_action.dart new file mode 100644 index 000000000..a9c2163d7 --- /dev/null +++ b/lib/features/push_notification/domain/model/web_socket_action.dart @@ -0,0 +1 @@ +enum WebSocketAction {connect, disconnect} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/web_socket_push_state.dart b/lib/features/push_notification/domain/state/web_socket_push_state.dart index ce827ed60..f6bdba4c9 100644 --- a/lib/features/push_notification/domain/state/web_socket_push_state.dart +++ b/lib/features/push_notification/domain/state/web_socket_push_state.dart @@ -5,12 +5,12 @@ import 'package:jmap_dart_client/jmap/push/state_change.dart'; class InitializingWebSocketPushChannel extends LoadingState {} class WebSocketPushStateReceived extends UIState { - final StateChange stateChange; + final StateChange? stateChange; WebSocketPushStateReceived(this.stateChange); @override - List get props => [stateChange]; + List get props => [stateChange]; } class WebSocketConnectionFailed extends FeatureFailure { diff --git a/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart b/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart index f7313152b..48cf8b725 100644 --- a/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart +++ b/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart @@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/push/state_change.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/web_socket_echo.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/web_socket_push_state.dart'; @@ -31,9 +32,21 @@ class ConnectWebSocketInteractor { } Either _toStateChange(dynamic data) { - if (data is String) { - data = jsonDecode(data); + StateChange? possibleStateChange; + try { + if (data is String) { + data = jsonDecode(data); + } + possibleStateChange = StateChange.fromJson(data); + return Right(WebSocketPushStateReceived(possibleStateChange)); + } catch (e) { + logError('ConnectWebSocketInteractor::_toStateChange: ' + 'websocket message is not StateChange: $data'); + final dataIsWebSocketEcho = WebSocketEcho.isValid(data); + if (dataIsWebSocketEcho) { + return Right(WebSocketPushStateReceived(null)); + } + return Left(WebSocketConnectionFailed(exception: e)); } - return Right(WebSocketPushStateReceived(StateChange.fromJson(data))); } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart index e9787a7ef..dde604088 100644 --- a/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart @@ -1,7 +1,7 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart'; -import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/web_socket_datasource_impl.dart'; import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart'; import 'package:tmail_ui_user/features/push_notification/data/repository/web_socket_repository_impl.dart'; 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'; class WebSocketInteractorBindings extends InteractorsBindings { @override void bindingsDataSource() { - Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override void bindingsDataSourceImpl() { - Get.lazyPut(() => RemoteWebSocketDatasourceImpl( + Get.lazyPut(() => WebSocketDatasourceImpl( Get.find(), Get.find(), )); diff --git a/lib/features/push_notification/presentation/controller/push_base_controller.dart b/lib/features/push_notification/presentation/controller/push_base_controller.dart index 608011be0..14ccdc735 100644 --- a/lib/features/push_notification/presentation/controller/push_base_controller.dart +++ b/lib/features/push_notification/presentation/controller/push_base_controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -17,12 +19,12 @@ abstract class PushBaseController { Session? session; AccountId? accountId; + StreamSubscription>? _stateStreamSubscription; + void consumeState(Stream> newStateStream) { - newStateStream.listen( + _stateStreamSubscription = newStateStream.listen( _handleStateStream, - onError: (error, stackTrace) { - logError('PushBaseController::consumeState():onError:error: $error | stackTrace: $stackTrace'); - } + onError: handleErrorViewState, ); } @@ -34,6 +36,15 @@ abstract class PushBaseController { void handleSuccessViewState(Success success); + void handleErrorViewState(Object error, StackTrace stackTrace) { + logError('PushBaseController::handleErrorViewState():error: $error | stackTrace: $stackTrace'); + } + + void cancelStateStreamSubscription() { + _stateStreamSubscription?.cancel(); + _stateStreamSubscription = null; + } + void initialize({AccountId? accountId, Session? session}) { this.accountId = accountId; this.session = session; @@ -87,22 +98,20 @@ abstract class PushBaseController { {Session? session} ) { final newState = jmap.State(mapTypeState[typeName.value]); - if (typeName == TypeName.emailType) { - if (isForeground) { - return SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session); - } else { - return StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session); - } - } else if (typeName == TypeName.emailDelivery) { - if (!isForeground) { - return PushNotificationAction(typeName, newState, session, accountId, userName); - } - } else if (typeName == TypeName.mailboxType) { - if (isForeground) { - return SynchronizeMailboxOnForegroundAction(typeName, newState, accountId); - } else { - return StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName); - } + switch (typeName) { + case TypeName.emailType: + return isForeground + ? SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session) + : StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session); + case TypeName.emailDelivery: + if (!isForeground) { + return PushNotificationAction(typeName, newState, session, accountId, userName); + } + break; + case TypeName.mailboxType: + return isForeground + ? SynchronizeMailboxOnForegroundAction(typeName, newState, accountId) + : StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName); } return null; } diff --git a/lib/features/push_notification/presentation/controller/web_socket_controller.dart b/lib/features/push_notification/presentation/controller/web_socket_controller.dart index b69cb664e..6632c78a5 100644 --- a/lib/features/push_notification/presentation/controller/web_socket_controller.dart +++ b/lib/features/push_notification/presentation/controller/web_socket_controller.dart @@ -3,6 +3,7 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/web_socket_push_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/connect_web_socket_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/controller/push_base_controller.dart'; @@ -20,9 +21,15 @@ class WebSocketController extends PushBaseController { ConnectWebSocketInteractor? _connectWebSocketInteractor; + int _retryRemained = 3; + @override void handleFailureViewState(Failure failure) { logError('WebSocketController::handleFailureViewState():Failure $failure'); + cancelStateStreamSubscription(); + if (failure is WebSocketConnectionFailed) { + _handleWebSocketConnectionRetry(); + } } @override @@ -33,6 +40,15 @@ class WebSocketController extends PushBaseController { } } + @override + void handleErrorViewState(Object error, StackTrace stackTrace) { + super.handleErrorViewState(error, stackTrace); + cancelStateStreamSubscription(); + if (error is WebSocketClosedException) { + _handleWebSocketConnectionRetry(); + } + } + @override void initialize({AccountId? accountId, Session? session}) { super.initialize(accountId: accountId, session: session); @@ -51,8 +67,10 @@ class WebSocketController extends PushBaseController { void _handleWebSocketPushStateReceived(WebSocketPushStateReceived success) { log('WebSocketController::_handleWebSocketPushStateReceived(): $success'); + _retryRemained = 3; if (accountId == null || session == null) return; final stateChange = success.stateChange; + if (stateChange == null) return; final mapTypeState = stateChange.getMapTypeState(accountId!); mappingTypeStateToAction( mapTypeState, @@ -62,4 +80,11 @@ class WebSocketController extends PushBaseController { session!.username, session: session); } + + void _handleWebSocketConnectionRetry() { + if (_retryRemained > 0) { + _retryRemained--; + _connectWebSocket(accountId, session); + } + } } \ No newline at end of file diff --git a/test/features/push_notification/data/model/web_socket_echo_test.dart b/test/features/push_notification/data/model/web_socket_echo_test.dart new file mode 100644 index 000000000..cd9145044 --- /dev/null +++ b/test/features/push_notification/data/model/web_socket_echo_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/web_socket_echo.dart'; + +void main() { + group('web socket echo test:', () { + group('isValid():', () { + test( + 'should return true ' + 'when json is web socket echo', + () { + // arrange + final json = { + "@type": "Response", + "requestId": "R1", + "methodResponses": [["Core/echo", {}, "c0"]] + }; + + // act + final result = WebSocketEcho.isValid(json); + + // assert + expect(result, true); + }); + + test( + 'should return false ' + 'when json is not web socket echo', + () { + // arrange + final json = { + "@type": "Response", + "requestId": "R1", + "methodResponses": [["Core/not-echo", {}, "c0"]] + }; + + // act + final result = WebSocketEcho.isValid(json); + + // assert + expect(result, false); + }); + }); + }); +} \ No newline at end of file diff --git a/test/features/push_notification/domain/usecases/connect_web_socket_interactor_test.dart b/test/features/push_notification/domain/usecases/connect_web_socket_interactor_test.dart new file mode 100644 index 000000000..06f1a609a --- /dev/null +++ b/test/features/push_notification/domain/usecases/connect_web_socket_interactor_test.dart @@ -0,0 +1,121 @@ +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/push/state_change.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/web_socket_push_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/connect_web_socket_interactor.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; +import 'connect_web_socket_interactor_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + late MockWebSocketRepository webSocketRepository; + late ConnectWebSocketInteractor connectWebSocketInteractor; + + setUp(() { + webSocketRepository = MockWebSocketRepository(); + connectWebSocketInteractor = ConnectWebSocketInteractor(webSocketRepository); + }); + + group('connect web socket interactor test:', () { + test( + 'should yield WebSocketPushStateReceived with StateChange ' + 'when web socket repository yield StateChange', + () { + // arrange + final stateChangeJson = { + "@type": "StateChange", + "changed": {} + }; + when(webSocketRepository.getWebSocketChannel(any, any)) + .thenAnswer((_) => Stream.value(stateChangeJson)); + + // assert + expect( + connectWebSocketInteractor.execute( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId), + emitsInOrder([ + Right(InitializingWebSocketPushChannel()), + Right(WebSocketPushStateReceived(StateChange.fromJson(stateChangeJson))), + ])); + }); + + test( + 'should yield WebSocketPushStateReceived with null ' + 'when web socket repository yield WebSocketEcho', + () { + // arrange + final webSocketEchoJson = { + "@type": "Response", + "requestId": "R1", + "methodResponses": [["Core/echo", {}, "c0"]] + }; + when(webSocketRepository.getWebSocketChannel(any, any)) + .thenAnswer((_) => Stream.value(webSocketEchoJson)); + + // assert + expect( + connectWebSocketInteractor.execute( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId), + emitsInOrder([ + Right(InitializingWebSocketPushChannel()), + Right(WebSocketPushStateReceived(null)), + ])); + }); + + test( + 'should yield WebSocketConnectionFailed ' + 'when web socket repository throws exception', + () { + // arrange + final exception = Exception(); + when(webSocketRepository.getWebSocketChannel(any, any)) + .thenThrow(exception); + + // assert + expect( + connectWebSocketInteractor.execute( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId), + emitsInOrder([ + Right(InitializingWebSocketPushChannel()), + Left(WebSocketConnectionFailed(exception: exception)), + ])); + }); + + test( + 'should yield WebSocketConnectionFailed ' + 'when web socket repository yield data ' + 'and data is not web socket echo', + () async { + // arrange + final notWebSocketEchoJson = { + "@type": "Response", + "requestId": "R1", + "methodResponses": [["Core/not-echo", {}, "c0"]] + }; + when(webSocketRepository.getWebSocketChannel(any, any)) + .thenAnswer((_) => Stream.value(notWebSocketEchoJson)); + + // act + final lastState = await connectWebSocketInteractor.execute( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId).last; + + // assert + expect( + lastState.fold( + (failure) => failure is WebSocketConnectionFailed, + (success) => false, + ), + true + ); + }); + }); +} \ No newline at end of file diff --git a/web/web-sockets-worker.js b/web/web-sockets-worker.js new file mode 100644 index 000000000..9205342bc --- /dev/null +++ b/web/web-sockets-worker.js @@ -0,0 +1,74 @@ +importScripts('flutter_service_worker.js?v={{flutter_service_worker_version}}'); + +var webSocket; +const broadcast = new BroadcastChannel("background-message"); +var intervalId; +const pingIntervalInMs = 10000; + +function connect(url, ticket) { + webSocket = new WebSocket(`${url}?ticket=${ticket}`, "jmap"); + + webSocket.onopen = () => { + console.log("websocket open"); + webSocket.send( + JSON.stringify({ + "@type": "WebSocketPushEnable", + dataTypes: ["Mailbox", "Email"], + }) + ); + intervalId = setInterval(() => { + webSocket.send( + JSON.stringify({ + "@type": "Request", + id: "R1", + using: ["urn:ietf:params:jmap:core"], + methodCalls: [["Core/echo", {}, "c0"]], + }) + ); + }, pingIntervalInMs); + }; + + webSocket.onmessage = (event) => { + console.log(`websocket received message: ${event.data}`); + broadcast.postMessage(event.data); + }; + + webSocket.onclose = (event) => { + console.log( + `websocket connection closed with code: "${event.code}" reason: "${event.reason}" and cleanly: "${event.wasClean}"` + ); + broadcast.postMessage("webSocketClosed"); + webSocket = null; + clearInterval(intervalId); + }; +} + +function disconnect() { + if (webSocket == null) { + return; + } + webSocket.close(); +} + +self.addEventListener("install", (event) => { + self.skipWaiting().then(() => { + console.log("Service worker installed"); + }); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + self.clients.claim().then(() => { + console.log("Service worker activated"); + }) + ); +}); + +self.addEventListener("message", (event) => { + console.log(`web socket worker received message: ${event.data}`); + if (event.data.action === "connect") { + connect(event.data.url, event.data.ticket); + } else if (event.data.action === "disconnect") { + disconnect(); + } +}); diff --git a/web/worker_service/worker_service.js b/web/worker_service/worker_service.js index eabbda0a4..2fac331b6 100644 --- a/web/worker_service/worker_service.js +++ b/web/worker_service/worker_service.js @@ -4,7 +4,6 @@ const iosPlatform = 'iOS'; const androidPlatform = 'android'; const otherPlatform = 'other'; const timeoutDuration = 4000; -var serviceWorkerVersion = null; var scriptLoaded = false; function loadMainDartJs() { @@ -24,16 +23,7 @@ function fetchServiceWorker() { // Wait for registration to finish before dropping the