import type { Action } from "@ngrx/store" ; import { Actions , createEffect } from "@ngrx/effects" ; import { EMPTY , Observable , concat , from , fromEvent , merge , of , race , throwError , timer } from "rxjs" ; import { Store , select } from "@ngrx/store" ; import { catchError , concatMap , debounce , debounceTime , delay , filter , finalize , first , map , mergeMap , switchMap , take , takeUntil , tap , withLatestFrom , } from "rxjs/operators" ; import { produce } from "immer" ; import { serializeError } from "serialize-error" ; import { ACCOUNTS_ACTIONS , DB_VIEW_ACTIONS , NAVIGATION_ACTIONS , NOTIFICATION_ACTIONS , OPTIONS_ACTIONS , } from "src/web/browser-window/app/store/actions" ; import { AccountsSelectors , OptionsSelectors } from "src/web/browser-window/app/store/selectors" ; import { AccountsService } from "src/web/browser-window/app/_accounts/accounts.service" ; import { CoreService } from "src/web/browser-window/app/_core/core.service" ; import { ElectronService } from "src/web/browser-window/app/_core/electron.service" ; import { FIRE_SYNCING_ITERATION$ } from "src/web/browser-window/app/app.constants" ; import { IPC_MAIN_API_NOTIFICATION_ACTIONS } from "src/shared/api/main-process/actions" ; import { Injectable } from "@angular/core" ; import { ONE_MINUTE_MS , ONE_SECOND_MS , PRODUCT_NAME } from "src/shared/constants" ; import { State } from "src/web/browser-window/app/store/reducers/accounts" ; import { consumeMemoryRateLimiter , curryFunctionMembers , isDatabaseBootstrapped , testProtonCalendarAppPage } from "src/shared/util" ; import { getWebLogger } from "src/web/browser-window/util" ; import { ofType } from "src/shared/ngrx-util-of-type" ; const RateLimiterMemory = require ( "rate-limiter-flexible/lib/RateLimiterMemory" ) ; const _logger = getWebLogger ( __filename ) ; const pingOnlineStatusEverySecond$ = timer ( 0 , ONE_SECOND_MS ) . pipe ( filter ( ( ) => navigator . onLine ) ) ; @ Injectable ( ) export class AccountsEffects { private readonly loginRateLimiterOptions = { points : 2 , duration : 10 , } as const ; private readonly loginRateLimiter = new RateLimiterMemory ( this . loginRateLimiterOptions ) ; syncAccountsConfigs$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( OPTIONS_ACTIONS . GetSettingsResponse ) , map ( ( { payload } ) => ACCOUNTS_ACTIONS . WireUpConfigs ( { accountConfigs : payload . accounts } ) ) ) ) ; setupPrimaryNotificationChannel$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . SetupPrimaryNotificationChannel ) , mergeMap ( ( { payload , ... action } ) => { const { webView , finishPromise , account : { accountIndex } } = payload ; const logger = curryFunctionMembers ( _logger , `[${ action . type }][${ accountIndex }]` ) ; const { login } = payload . account . accountConfig ; const dispose$ = from ( finishPromise ) . pipe ( tap ( ( ) => { logger . info ( "dispose" ) ; this . store . dispatch ( this . accountsService . generatePrimaryNotificationsStateResetAction ( { login , optionalAccount : true } ) ) ; } ) ) ; const parsedEntryUrlBundle = this . core . parseEntryUrl ( payload . account . accountConfig , "proton-mail" ) ; logger . info ( "setup" ) ; return merge ( this . api . primaryWebViewClient ( { webView , accountIndex } , { finishPromise } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "notification" ) ( { ... parsedEntryUrlBundle , login , accountIndex } ) ) ; } ) , withLatestFrom ( this . store . pipe ( select ( AccountsSelectors . ACCOUNTS . pickAccount ( { login } ) ) ) ) , mergeMap ( ( [ notification , account ] ) => { if ( typeof notification . batchEntityUpdatesCounter !== "undefined" ) { FIRE_SYNCING_ITERATION$ . next ( { login } ) ; return EMPTY ; } if ( account && account . syncingActivated && typeof notification . unread === "number" ) { return EMPTY ; } return of ( ACCOUNTS_ACTIONS . Patch ( { login , patch : { notifications : notification } } ) ) ; } ) ) , this . store . pipe ( select ( OptionsSelectors . FEATURED . mainProcessNotificationAction ) , ofType ( IPC_MAIN_API_NOTIFICATION_ACTIONS . DbAttachmentExportRequest ) , filter ( ( { payload : { key } } ) => key . login === login ) , mergeMap ( ( { payload } ) => { return this . api . primaryWebViewClient ( { webView , accountIndex } , { finishPromise } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "exportMailAttachments" , { timeoutMs : payload . timeoutMs } ) ( { uuid : payload . uuid , mailPk : payload . mailPk , login : payload . key . login , accountIndex , } ) ) ; } ) , catchError ( ( error ) => { return from ( this . api . ipcMainClient ( ) ( "dbExportMailAttachmentsNotification" ) ( { uuid : payload . uuid , accountPk : { login } , attachments : [ ] , serializedError : serializeError ( error ) , } ) ) . pipe ( mergeMap ( ( ) => throwError ( error ) ) ) ; } ) , mergeMap ( ( ) => EMPTY ) ) ; } ) ) ) . pipe ( takeUntil ( dispose$ ) ) ; } ) ) ) ; setupCalendarNotificationChannel$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . SetupCalendarNotificationChannel ) , mergeMap ( ( { payload , ... action } ) => { const { webView , finishPromise , account : { accountIndex } } = payload ; const logger = curryFunctionMembers ( _logger , `[${ action . type }][${ accountIndex }]` ) ; const { login } = payload . account . accountConfig ; const dispose$ = from ( finishPromise ) . pipe ( tap ( ( ) => { logger . info ( "dispose" ) ; this . store . dispatch ( this . accountsService . generateCalendarNotificationsStateResetAction ( { login , optionalAccount : true } ) ) ; } ) ) ; logger . info ( "setup" ) ; return merge ( this . api . calendarWebViewClient ( { webView , accountIndex } , { finishPromise } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "notification" ) ( { accountIndex } ) ) ; } ) , withLatestFrom ( this . store . pipe ( select ( OptionsSelectors . FEATURED . trayIconDataURL ) ) , this . store . pipe ( select ( AccountsSelectors . ACCOUNTS . pickAccount ( { login } ) ) , mergeMap ( ( account ) => account ? [ account ] : EMPTY ) ) ) , mergeMap ( ( [ notification , trayIconDataURL , { webviewSrcValues } ] ) => { if ( ! ( "calendarNotification" in notification ) ) { return of ( ACCOUNTS_ACTIONS . Patch ( { login , patch : { notifications : notification } } ) ) ; } if ( ! notification . calendarNotification ) { throw new Error ( `Unexpected "Notification" constructor args received.` ) ; } if ( testProtonCalendarAppPage ( { url : webviewSrcValues . primary , logger } ) . projectType === "proton-calendar" ) { return EMPTY ; } const [ title , options ] = notification . calendarNotification ; new Notification ( `${ PRODUCT_NAME }: ${ title }` , { icon : trayIconDataURL , body : typeof options === "object" ? options . body : options , } ) . onclick = ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . Select ( { login } ) ) ; this . store . dispatch ( NAVIGATION_ACTIONS . ToggleBrowserWindow ( { forcedState : true } ) ) ; } ; return EMPTY ; } ) ) ) . pipe ( takeUntil ( dispose$ ) ) ; } ) ) ) ; toggleSyncing$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . ToggleSyncing ) , mergeMap ( ( { payload , ... action } ) => { const { pk : { login , accountIndex } , webView , finishPromise } = payload ; const logger = curryFunctionMembers ( _logger , `[${ action . type }][${ accountIndex }]` ) ; const dispose$ = from ( finishPromise ) . pipe ( tap ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . Patch ( { login , patch : { syncingActivated : false } , optionalAccount : true } ) ) ; logger . info ( "dispose" ) ; } ) ) ; const notSyncingPing$ = timer ( 0 , ONE_SECOND_MS ) . pipe ( switchMap ( ( ) => this . store . pipe ( select ( AccountsSelectors . ACCOUNTS . pickAccount ( { login } ) ) , filter ( ( account ) => Boolean ( account && ! account . progress . syncing ) ) ) ) ) ; const ipcMainClient = this . api . ipcMainClient ( ) ; let bootstrappingTriggeredOnce = false ; logger . info ( "setup" ) ; return merge ( of ( ACCOUNTS_ACTIONS . Patch ( { login , patch : { syncingActivated : true } } ) ) , this . store . pipe ( select ( OptionsSelectors . FEATURED . mainProcessNotificationAction ) , ofType ( IPC_MAIN_API_NOTIFICATION_ACTIONS . DbPatchAccount ) , filter ( ( { payload : { key } } ) => key . login === login ) , mergeMap ( ( { payload } ) => of ( ACCOUNTS_ACTIONS . Patch ( { login , patch : { notifications : { unread : payload . stat . unread } } } ) ) ) ) , createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . SelectMailOnline ) , filter ( ( { payload : { pk : key } } ) => key . login === login ) , mergeMap ( ( { payload : selectMailOnlineInput } ) => concat ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { selectingMailOnline : true } } ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } , { finishPromise } ) . pipe ( mergeMap ( ( webViewClient ) => { const selectMailOnlineInput$ = from ( webViewClient ( "selectMailOnline" , { timeoutMs : ONE_SECOND_MS * 5 } ) ( { ... selectMailOnlineInput , accountIndex , } ) ) ; return selectMailOnlineInput$ . pipe ( mergeMap ( ( ) => EMPTY ) , finalize ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { selectingMailOnline : false } , optionalAccount : true } ) ) ; } ) ) ; } ) ) ) ) ) ) , createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . MakeMailRead ) , filter ( ( { payload } ) => payload . pk . login === login ) , mergeMap ( ( { payload : { messageIds } } ) => concat ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { makingMailRead : true } } ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "makeMailRead" , { timeoutMs : ONE_SECOND_MS * 30 } ) ( { messageIds , accountIndex } ) ) . pipe ( mergeMap ( ( ) => this . core . fireSyncingIteration ( { login } ) ) , finalize ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { makingMailRead : false } , optionalAccount : true } ) ) ; } ) ) ; } ) ) ) ) ) , { dispatch : false } ) , createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . SetMailFolder ) , filter ( ( { payload } ) => payload . pk . login === login ) , mergeMap ( ( { payload : { folderId , messageIds } } ) => concat ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { settingMailFolder : true } } ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "setMailFolder" , { timeoutMs : ONE_MINUTE_MS } ) ( { folderId , messageIds , accountIndex } ) ) . pipe ( mergeMap ( ( ) => this . core . fireSyncingIteration ( { login } ) ) , finalize ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { settingMailFolder : false } , optionalAccount : true } ) ) ; } ) ) ; } ) ) ) ) ) , { dispatch : false } ) , createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . DeleteMessages ) , filter ( ( { payload } ) => payload . pk . login === login ) , mergeMap ( ( { payload : { messageIds } } ) => concat ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { deletingMessages : true } } ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "deleteMessages" , { timeoutMs : ONE_MINUTE_MS } ) ( { messageIds , accountIndex } ) ) . pipe ( mergeMap ( ( ) => this . core . fireSyncingIteration ( { login } ) ) , finalize ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { deletingMessages : false } , optionalAccount : true } ) ) ; } ) ) ; } ) ) ) ) ) , { dispatch : false } ) , createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . FetchSingleMail ) , filter ( ( { payload } ) => payload . pk . login === login ) , mergeMap ( ( { payload : { pk , mailPk } } ) => concat ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { fetchingSingleMail : true } } ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "fetchSingleMail" ) ( { ... pk , mailPk , accountIndex } ) ) . pipe ( mergeMap ( ( ) => of < Action > ( DB_VIEW_ACTIONS . SelectConversationMailRequest ( { webAccountPk : pk , mailPk } ) ) ) , finalize ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { fetchingSingleMail : false } , optionalAccount : true } ) ) ; } ) ) ; } ) ) ) ) ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } , { finishPromise } ) . pipe ( withLatestFrom ( this . store . pipe ( select ( OptionsSelectors . FEATURED . config ) ) ) , mergeMap ( ( [ webViewClient , { dbSyncingIntervalTrigger , dbSyncingOnlineTriggerDelay , dbSyncingFiredTriggerDebounce } ] ) => { const syncingIterationTrigger$ = merge ( timer ( 0 , dbSyncingIntervalTrigger ) . pipe ( tap ( ( ) => logger . verbose ( `triggered by: timer` ) ) , map ( ( ) => null ) ) , fromEvent ( window , "online" ) . pipe ( tap ( ( ) => logger . verbose ( `triggered by: "window.online" event` ) ) , delay ( dbSyncingOnlineTriggerDelay ) , map ( ( ) => null ) ) , FIRE_SYNCING_ITERATION$ . pipe ( filter ( ( value ) => value . login === login ) , tap ( ( ) => logger . verbose ( `triggered by: FIRE_SYNCING_ITERATION$` ) ) , debounceTime ( dbSyncingFiredTriggerDebounce ) ) ) . pipe ( map ( ( ) => null ) ) ; return syncingIterationTrigger$ . pipe ( debounceTime ( ONE_SECOND_MS ) , debounce ( ( ) => pingOnlineStatusEverySecond$ ) , debounce ( ( ) => notSyncingPing$ ) , concatMap ( ( ) => { return from ( ipcMainClient ( "dbGetAccountMetadata" ) ( { login } ) ) ; } ) , withLatestFrom ( this . store . pipe ( select ( OptionsSelectors . CONFIG . timeouts ) ) ) , concatMap ( ( [ metadata , timeouts ] ) => { const bootstrapping = ! isDatabaseBootstrapped ( metadata ) ; const buildDbPatchMethodName = "buildDbPatch" ; if ( bootstrapping && bootstrappingTriggeredOnce ) { return throwError ( new Error ( `Database bootstrap fetch has already been called once for the account, ${ accountIndex }` ) ) ; } const timeoutMs = bootstrapping ? timeouts . dbBootstrapping : timeouts . dbSyncing ; logger . verbose ( `calling "${ buildDbPatchMethodName }" api` , JSON . stringify ( { timeoutMs , bootstrapping , bootstrappingTriggeredOnce , } ) ) ; this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { syncing : true } } ) ) ; const result$ = from ( webViewClient ( buildDbPatchMethodName , { timeoutMs } ) ( { login , accountIndex , metadata , } ) . pipe ( tap ( ( value ) => { logger . verbose ( `${ buildDbPatchMethodName }:value type: ${ typeof value }` ) ; } ) , catchError ( ( error ) => { return of ( NOTIFICATION_ACTIONS . Error ( error ) ) ; } ) ) ) . pipe ( concatMap ( ( ) => of ( ACCOUNTS_ACTIONS . Synced ( { pk : { login , accountIndex } } ) ) ) , takeUntil ( fromEvent ( window , "offline" ) . pipe ( tap ( ( ) => { logger . verbose ( `offline event` ) ; if ( bootstrapping && bootstrappingTriggeredOnce ) { bootstrappingTriggeredOnce = false ; logger . verbose ( [ `reset "bootstrappingTriggeredOnce" state as previous iteration` , `got aborted by the "offline" event` , ] . join ( " " ) ) ; } } ) ) ) , finalize ( ( ) => { this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { syncing : false } , optionalAccount : true } ) ) ; } ) ) ; if ( bootstrapping ) { bootstrappingTriggeredOnce = true ; logger . verbose ( "bootstrappingTriggeredOnce = true" ) ; } return result$ ; } ) ) ; } ) ) ) . pipe ( takeUntil ( dispose$ ) ) ; } ) ) ) ; tryToLogin$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . TryToLogin ) , mergeMap ( ( { payload , ... action } ) => { const { account , webView } = payload ; const { accountConfig , notifications , accountIndex } = account ; const logger = curryFunctionMembers ( _logger , `[${ action . type }][${ accountIndex }]` ) ; const { login , credentials } = accountConfig ; const { type : pageType , skipLoginDelayLogic } = notifications . pageType ; const resetNotificationsState$ = of ( this . accountsService . generatePrimaryNotificationsStateResetAction ( { login } ) ) ; const rateLimitCheck = async ( password ) : Promise < void > => { const key = String ( [ login , pageType , password ] ) ; const { waitTimeMs } = await consumeMemoryRateLimiter ( async ( ) => this . loginRateLimiter . consume ( key ) ) ; if ( waitTimeMs > 0 ) { const { points , duration } = this . loginRateLimiterOptions ; throw new Error ( [ `It's not allowed to submit the same password for the same account` , `more than ${ points } times per ${ duration } milliseconds (${ JSON . stringify ( { pageType , waitTimeMs } ) }).` , `Make sure that your login/password is correct.` , `Auto login feature is disable until app restarted.` , ] . join ( " " ) ) ; } } ; logger . verbose ( JSON . stringify ( { pageType , skipLoginDelayLogic } ) ) ; switch ( pageType ) { case "login" : { const onlyFillLoginAction = ( ) : Observable < import ( "@ngrx/store" ) . Action > => { logger . info ( "fillLogin" ) ; return merge ( of ( this . accountsService . buildLoginDelaysResetAction ( { login } ) ) , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "fillLogin" ) ( { login , accountIndex } ) ) ; } ) , mergeMap ( ( ) => of ( ACCOUNTS_ACTIONS . Patch ( { login , patch : { loginFilledOnce : true } } ) ) ) ) ) ; } ; const fullLoginAction = ( ) : Observable < import ( "@ngrx/store" ) . Action > => { const executeLoginAction = ( password ) : Observable < import ( "@ngrx/store" ) . Action > => { logger . info ( "login" ) ; const action$ = merge ( of ( this . accountsService . buildLoginDelaysResetAction ( { login } ) ) , of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { password : true } } ) ) , resetNotificationsState$ , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( delay ( account . loggedInOnce ? ONE_SECOND_MS : 0 ) , mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "login" ) ( { login , password , accountIndex } ) ) ; } ) , mergeMap ( ( ) => EMPTY ) , finalize ( ( ) => this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { password : false } } ) ) ) ) ) ; return from ( rateLimitCheck ( password ) ) . pipe ( concatMap ( ( ) => action$ ) ) ; } ; const trigger$ = skipLoginDelayLogic ? of ( { trigger : "the delay already took place, so immediate resolve" } ) : this . accountsService . setupLoginDelayTrigger ( { login } , logger ) ; return trigger$ . pipe ( mergeMap ( ( ) => this . store . pipe ( select ( AccountsSelectors . ACCOUNTS . pickAccount ( { login } ) ) , take ( 1 ) , mergeMap ( ( value ) => { if ( ! value ) { logger . info ( "account got removed during login delaying?" ) ; return EMPTY ; } return [ { password : value . accountConfig . credentials . password } ] ; } ) , mergeMap ( ( { password } ) => { if ( ! password ) { logger . info ( "login action canceled due to the empty password" ) ; return EMPTY ; } return executeLoginAction ( password ) ; } ) ) ) ) ; } ; return from ( this . api . ipcMainClient ( ) ( "resetProtonBackendSession" ) ( { login } ) ) . pipe ( mergeMap ( ( ) => { if ( ! credentials . password ) { return onlyFillLoginAction ( ) ; } return fullLoginAction ( ) ; } ) ) ; } case "login2fa" : { const { twoFactorCode : secret } = credentials ; if ( ! secret ) { break ; } const action$ = merge ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { twoFactorCode : true } } ) ) , resetNotificationsState$ , this . api . primaryWebViewClient ( { webView , accountIndex } ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "login2fa" ) ( { secret , accountIndex } ) ) ; } ) , mergeMap ( ( ) => EMPTY ) , finalize ( ( ) => this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { twoFactorCode : false } } ) ) ) ) ) ; logger . info ( "login2fa" ) ; return from ( rateLimitCheck ( secret ) ) . pipe ( concatMap ( ( ) => action$ ) ) ; } case "unlock" : { const mailPassword = "mailPassword" in credentials && credentials . mailPassword ; if ( ! mailPassword ) { break ; } const action$ = merge ( of ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { mailPassword : true } } ) ) , resetNotificationsState$ , of ( __ELECTRON_EXPOSURE__ . buildIpcPrimaryWebViewClient ( webView ) ) . pipe ( mergeMap ( ( webViewClient ) => { return from ( webViewClient ( "unlock" ) ( { mailPassword , accountIndex } ) ) ; } ) , mergeMap ( ( ) => EMPTY ) , finalize ( ( ) => this . store . dispatch ( ACCOUNTS_ACTIONS . PatchProgress ( { login , patch : { mailPassword : false } } ) ) ) ) ) ; logger . info ( "unlock" ) ; return from ( rateLimitCheck ( mailPassword ) ) . pipe ( concatMap ( ( ) => action$ ) ) ; } } logger . verbose ( "empty" ) ; return [ ] ; } ) ) ) ; unload$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . Unload ) , withLatestFrom ( this . store . pipe ( select ( AccountsSelectors . FEATURED . accounts ) ) ) , concatMap ( ( [ { payload } , accounts ] ) => { const accountConfigs = accounts . map ( ( { accountConfig } ) => accountConfig ) ; const renderedAccountsTimeoutMs = ONE_SECOND_MS ; return concat ( of ( ACCOUNTS_ACTIONS . WireUpConfigs ( { accountConfigs : produce ( accountConfigs , ( draftState ) => { const accountToUnload = draftState . find ( ( { login } ) => login === payload . login ) ; if ( ! accountToUnload ) { throw new Error ( "Failed to resolve account by login." ) ; } accountToUnload . disabled = true ; } ) , } ) ) , race ( this . actions$ . pipe ( ofType ( ACCOUNTS_ACTIONS . UnloadRollback ) , filter ( ( { payload : { uuid } } ) => uuid === payload . uuid ) , first ( ) , concatMap ( ( ) => of ( ACCOUNTS_ACTIONS . WireUpConfigs ( { accountConfigs } ) ) ) ) , timer ( renderedAccountsTimeoutMs ) . pipe ( concatMap ( ( ) => throwError ( ( ) => new Error ( `Failed to received response in ${ renderedAccountsTimeoutMs }ms` ) ) ) ) ) ) ; } ) ) ) ; constructor ( private readonly actions$ , private readonly api , private readonly core , private readonly store , private readonly accountsService ) { } }