From 25f55e13683e6fd198402f26afd25f10ed44c86f Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 15 May 2024 12:11:06 -0400 Subject: [PATCH] [PM-7978] Create ForegroundSyncService For Delegating `fullSync` Calls (#9192) * Create ForegroundSyncService For Delegating `fullSync` calls to the background * Relax `isExternalMessage` to Allow For Typed Payload * Null Coalesce The `startListening` Method * Filter To Only External Messages --- .../browser/src/background/main.background.ts | 76 ++++-- .../platform/sync/foreground-sync.service.ts | 79 ++++++ .../platform/sync/sync-service.listener.ts | 25 ++ libs/common/src/platform/messaging/helpers.ts | 4 +- .../src/platform/sync/core-sync.service.ts | 230 +++++++++++++++++ libs/common/src/platform/sync/internal.ts | 1 + .../src/vault/services/sync/sync.service.ts | 243 +++--------------- 7 files changed, 422 insertions(+), 236 deletions(-) create mode 100644 apps/browser/src/platform/sync/foreground-sync.service.ts create mode 100644 apps/browser/src/platform/sync/sync-service.listener.ts create mode 100644 libs/common/src/platform/sync/core-sync.service.ts create mode 100644 libs/common/src/platform/sync/internal.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c3722f2a480..d5e8fe1da74 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -230,6 +230,8 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; +import { ForegroundSyncService } from "../platform/sync/foreground-sync.service"; +import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; @@ -339,6 +341,7 @@ export default class MainBackground { scriptInjectorService: BrowserScriptInjectorService; kdfConfigService: kdfConfigServiceAbstraction; offscreenDocumentService: OffscreenDocumentService; + syncServiceListener: SyncServiceListener; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -792,32 +795,52 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); - this.syncService = new SyncService( - this.masterPasswordService, - this.accountService, - this.apiService, - this.domainSettingsService, - this.folderService, - this.cipherService, - this.cryptoService, - this.collectionService, - this.messagingService, - this.policyService, - this.sendService, - this.logService, - this.keyConnectorService, - this.stateService, - this.providerService, - this.folderApiService, - this.organizationService, - this.sendApiService, - this.userDecryptionOptionsService, - this.avatarService, - logoutCallback, - this.billingAccountProfileStateService, - this.tokenService, - this.authService, - ); + if (this.popupOnlyContext) { + this.syncService = new ForegroundSyncService( + this.stateService, + this.folderService, + this.folderApiService, + this.messagingService, + this.logService, + this.cipherService, + this.collectionService, + this.apiService, + this.accountService, + this.authService, + this.sendService, + this.sendApiService, + messageListener, + ); + } else { + this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, + this.apiService, + this.domainSettingsService, + this.folderService, + this.cipherService, + this.cryptoService, + this.collectionService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.keyConnectorService, + this.stateService, + this.providerService, + this.folderApiService, + this.organizationService, + this.sendApiService, + this.userDecryptionOptionsService, + this.avatarService, + logoutCallback, + this.billingAccountProfileStateService, + this.tokenService, + this.authService, + ); + + this.syncServiceListener = new SyncServiceListener(this.syncService, messageListener); + } this.eventUploadService = new EventUploadService( this.apiService, this.stateProvider, @@ -1141,6 +1164,7 @@ export default class MainBackground { this.contextMenusBackground?.init(); await this.idleBackground.init(); this.webRequestBackground?.startListening(); + this.syncServiceListener?.startListening(); return new Promise((resolve) => { setTimeout(async () => { diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts new file mode 100644 index 00000000000..3c144316724 --- /dev/null +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -0,0 +1,79 @@ +import { firstValueFrom, timeout } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { + CommandDefinition, + MessageListener, + MessageSender, +} from "@bitwarden/common/platform/messaging"; +import { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; + +const SYNC_COMPLETED = new CommandDefinition<{ successfully: boolean }>("syncCompleted"); +export const DO_FULL_SYNC = new CommandDefinition<{ + forceSync: boolean; + allowThrowOnError: boolean; +}>("doFullSync"); + +export class ForegroundSyncService extends CoreSyncService { + constructor( + stateService: StateService, + folderService: InternalFolderService, + folderApiService: FolderApiServiceAbstraction, + messageSender: MessageSender, + logService: LogService, + cipherService: CipherService, + collectionService: CollectionService, + apiService: ApiService, + accountService: AccountService, + authService: AuthService, + sendService: InternalSendService, + sendApiService: SendApiService, + private readonly messageListener: MessageListener, + ) { + super( + stateService, + folderService, + folderApiService, + messageSender, + logService, + cipherService, + collectionService, + apiService, + accountService, + authService, + sendService, + sendApiService, + ); + } + + async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise { + this.syncInProgress = true; + try { + const syncCompletedPromise = firstValueFrom( + this.messageListener.messages$(SYNC_COMPLETED).pipe( + timeout({ + first: 10_000, + with: () => { + throw new Error("Timeout while doing a fullSync call."); + }, + }), + ), + ); + this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError }); + const result = await syncCompletedPromise; + return result.successfully; + } finally { + this.syncInProgress = false; + } + } +} diff --git a/apps/browser/src/platform/sync/sync-service.listener.ts b/apps/browser/src/platform/sync/sync-service.listener.ts new file mode 100644 index 00000000000..b9e18accacd --- /dev/null +++ b/apps/browser/src/platform/sync/sync-service.listener.ts @@ -0,0 +1,25 @@ +import { Subscription, concatMap, filter } from "rxjs"; + +import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; + +import { DO_FULL_SYNC } from "./foreground-sync.service"; + +export class SyncServiceListener { + constructor( + private readonly syncService: SyncService, + private readonly messageListener: MessageListener, + ) {} + + startListening(): Subscription { + return this.messageListener + .messages$(DO_FULL_SYNC) + .pipe( + filter((message) => isExternalMessage(message)), + concatMap(async ({ forceSync, allowThrowOnError }) => { + await this.syncService.fullSync(forceSync, allowThrowOnError); + }), + ) + .subscribe(); + } +} diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/common/src/platform/messaging/helpers.ts index bf119432e05..ba772e517bc 100644 --- a/libs/common/src/platform/messaging/helpers.ts +++ b/libs/common/src/platform/messaging/helpers.ts @@ -12,8 +12,8 @@ export const getCommand = (commandDefinition: CommandDefinition | string export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); -export const isExternalMessage = (message: Message) => { - return (message as Record)?.[EXTERNAL_SOURCE_TAG] === true; +export const isExternalMessage = (message: Record) => { + return message?.[EXTERNAL_SOURCE_TAG] === true; }; export const tagAsExternal: MonoTypeOperatorFunction> = map( diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts new file mode 100644 index 00000000000..52c1a51cb82 --- /dev/null +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -0,0 +1,230 @@ +import { firstValueFrom, map, of, switchMap } from "rxjs"; + +import { ApiService } from "../../abstractions/api.service"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../../models/response/notification.response"; +import { SendData } from "../../tools/send/models/data/send.data"; +import { SendApiService } from "../../tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "../../tools/send/services/send.service.abstraction"; +import { CipherService } from "../../vault/abstractions/cipher.service"; +import { CollectionService } from "../../vault/abstractions/collection.service"; +import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; +import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; +import { CipherData } from "../../vault/models/data/cipher.data"; +import { FolderData } from "../../vault/models/data/folder.data"; +import { LogService } from "../abstractions/log.service"; +import { StateService } from "../abstractions/state.service"; +import { MessageSender } from "../messaging"; + +/** + * Core SyncService Logic EXCEPT for fullSync so that implementations can differ. + */ +export abstract class CoreSyncService implements SyncService { + syncInProgress = false; + + constructor( + protected readonly stateService: StateService, + protected readonly folderService: InternalFolderService, + protected readonly folderApiService: FolderApiServiceAbstraction, + protected readonly messageSender: MessageSender, + protected readonly logService: LogService, + protected readonly cipherService: CipherService, + protected readonly collectionService: CollectionService, + protected readonly apiService: ApiService, + protected readonly accountService: AccountService, + protected readonly authService: AuthService, + protected readonly sendService: InternalSendService, + protected readonly sendApiService: SendApiService, + ) {} + + abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; + + async getLastSync(): Promise { + if ((await this.stateService.getUserId()) == null) { + return null; + } + + const lastSync = await this.stateService.getLastSync(); + if (lastSync) { + return new Date(lastSync); + } + + return null; + } + + async setLastSync(date: Date, userId?: string): Promise { + await this.stateService.setLastSync(date.toJSON(), { userId: userId }); + } + + async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + const localFolder = await this.folderService.get(notification.id); + if ( + (!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate) + ) { + const remoteFolder = await this.folderApiService.get(notification.id); + if (remoteFolder != null) { + await this.folderService.upsert(new FolderData(remoteFolder)); + this.messageSender.send("syncedUpsertedFolder", { folderId: notification.id }); + return this.syncCompleted(true); + } + } + } catch (e) { + this.logService.error(e); + } + } + return this.syncCompleted(false); + } + + async syncDeleteFolder(notification: SyncFolderNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.folderService.delete(notification.id); + this.messageSender.send("syncedDeletedFolder", { folderId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + let shouldUpdate = true; + const localCipher = await this.cipherService.get(notification.id); + if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { + shouldUpdate = false; + } + + let checkCollections = false; + if (shouldUpdate) { + if (isEdit) { + shouldUpdate = localCipher != null; + checkCollections = true; + } else { + if (notification.collectionIds == null || notification.organizationId == null) { + shouldUpdate = localCipher == null; + } else { + shouldUpdate = false; + checkCollections = true; + } + } + } + + if ( + !shouldUpdate && + checkCollections && + notification.organizationId != null && + notification.collectionIds != null && + notification.collectionIds.length > 0 + ) { + const collections = await this.collectionService.getAll(); + if (collections != null) { + for (let i = 0; i < collections.length; i++) { + if (notification.collectionIds.indexOf(collections[i].id) > -1) { + shouldUpdate = true; + break; + } + } + } + } + + if (shouldUpdate) { + const remoteCipher = await this.apiService.getFullCipherDetails(notification.id); + if (remoteCipher != null) { + await this.cipherService.upsert(new CipherData(remoteCipher)); + this.messageSender.send("syncedUpsertedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + } + } catch (e) { + if (e != null && e.statusCode === 404 && isEdit) { + await this.cipherService.delete(notification.id); + this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + } + } + return this.syncCompleted(false); + } + + async syncDeleteCipher(notification: SyncCipherNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.cipherService.delete(notification.id); + this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + return this.syncCompleted(false); + } + + async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { + this.syncStarted(); + const [activeUserId, status] = await firstValueFrom( + this.accountService.activeAccount$.pipe( + switchMap((a) => { + if (a == null) { + of([null, AuthenticationStatus.LoggedOut]); + } + return this.authService.authStatusFor$(a.id).pipe(map((s) => [a.id, s])); + }), + ), + ); + // Process only notifications for currently active user when user is not logged out + // TODO: once send service allows data manipulation of non-active users, this should process any received notification + if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) { + try { + const localSend = await firstValueFrom(this.sendService.get$(notification.id)); + if ( + (!isEdit && localSend == null) || + (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) + ) { + const remoteSend = await this.sendApiService.getSend(notification.id); + if (remoteSend != null) { + await this.sendService.upsert(new SendData(remoteSend)); + this.messageSender.send("syncedUpsertedSend", { sendId: notification.id }); + return this.syncCompleted(true); + } + } + } catch (e) { + this.logService.error(e); + } + } + return this.syncCompleted(false); + } + + async syncDeleteSend(notification: SyncSendNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.sendService.delete(notification.id); + this.messageSender.send("syncedDeletedSend", { sendId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + // Helpers + + protected syncStarted() { + this.syncInProgress = true; + this.messageSender.send("syncStarted"); + } + + protected syncCompleted(successfully: boolean): boolean { + this.syncInProgress = false; + this.messageSender.send("syncCompleted", { successfully: successfully }); + return successfully; + } +} diff --git a/libs/common/src/platform/sync/internal.ts b/libs/common/src/platform/sync/internal.ts new file mode 100644 index 00000000000..f515e90a07e --- /dev/null +++ b/libs/common/src/platform/sync/internal.ts @@ -0,0 +1 @@ +export { CoreSyncService } from "./core-sync.service"; diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index b591a61e862..172891b08db 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, of, switchMap } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -17,22 +17,17 @@ import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; import { TokenService } from "../../../auth/abstractions/token.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; import { DomainsResponse } from "../../../models/response/domains.response"; -import { - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from "../../../models/response/notification.response"; import { ProfileResponse } from "../../../models/response/profile.response"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { LogService } from "../../../platform/abstractions/log.service"; -import { MessagingService } from "../../../platform/abstractions/messaging.service"; import { StateService } from "../../../platform/abstractions/state.service"; +import { MessageSender } from "../../../platform/messaging"; import { sequentialize } from "../../../platform/misc/sequentialize"; +import { CoreSyncService } from "../../../platform/sync/core-sync.service"; import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; @@ -40,7 +35,6 @@ import { InternalSendService } from "../../../tools/send/services/send.service.a import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; -import { SyncService as SyncServiceAbstraction } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { CipherData } from "../../../vault/models/data/cipher.data"; import { FolderData } from "../../../vault/models/data/folder.data"; import { CipherResponse } from "../../../vault/models/response/cipher.response"; @@ -49,55 +43,51 @@ import { CollectionService } from "../../abstractions/collection.service"; import { CollectionData } from "../../models/data/collection.data"; import { CollectionDetailsResponse } from "../../models/response/collection.response"; -export class SyncService implements SyncServiceAbstraction { - syncInProgress = false; - +export class SyncService extends CoreSyncService { constructor( private masterPasswordService: InternalMasterPasswordServiceAbstraction, - private accountService: AccountService, - private apiService: ApiService, + accountService: AccountService, + apiService: ApiService, private domainSettingsService: DomainSettingsService, - private folderService: InternalFolderService, - private cipherService: CipherService, + folderService: InternalFolderService, + cipherService: CipherService, private cryptoService: CryptoService, - private collectionService: CollectionService, - private messagingService: MessagingService, + collectionService: CollectionService, + messageSender: MessageSender, private policyService: InternalPolicyService, - private sendService: InternalSendService, - private logService: LogService, + sendService: InternalSendService, + logService: LogService, private keyConnectorService: KeyConnectorService, - private stateService: StateService, + stateService: StateService, private providerService: ProviderService, - private folderApiService: FolderApiServiceAbstraction, + folderApiService: FolderApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, - private sendApiService: SendApiService, + sendApiService: SendApiService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, private tokenService: TokenService, - private authService: AuthService, - ) {} - - async getLastSync(): Promise { - if ((await this.stateService.getUserId()) == null) { - return null; - } - - const lastSync = await this.stateService.getLastSync(); - if (lastSync) { - return new Date(lastSync); - } - - return null; - } - - async setLastSync(date: Date, userId?: string): Promise { - await this.stateService.setLastSync(date.toJSON(), { userId: userId }); + authService: AuthService, + ) { + super( + stateService, + folderService, + folderApiService, + messageSender, + logService, + cipherService, + collectionService, + apiService, + accountService, + authService, + sendService, + sendApiService, + ); } @sequentialize(() => "fullSync") - async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { + override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { this.syncStarted(); const isAuthenticated = await this.stateService.getIsAuthenticated(); if (!isAuthenticated) { @@ -110,6 +100,7 @@ export class SyncService implements SyncServiceAbstraction { needsSync = await this.needsSyncing(forceSync); } catch (e) { if (allowThrowOnError) { + this.syncCompleted(false); throw e; } } @@ -135,6 +126,7 @@ export class SyncService implements SyncServiceAbstraction { return this.syncCompleted(true); } catch (e) { if (allowThrowOnError) { + this.syncCompleted(false); throw e; } else { return this.syncCompleted(false); @@ -142,171 +134,6 @@ export class SyncService implements SyncServiceAbstraction { } } - async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - try { - const localFolder = await this.folderService.get(notification.id); - if ( - (!isEdit && localFolder == null) || - (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate) - ) { - const remoteFolder = await this.folderApiService.get(notification.id); - if (remoteFolder != null) { - await this.folderService.upsert(new FolderData(remoteFolder)); - this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - this.logService.error(e); - } - } - return this.syncCompleted(false); - } - - async syncDeleteFolder(notification: SyncFolderNotification): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.folderService.delete(notification.id); - this.messagingService.send("syncedDeletedFolder", { folderId: notification.id }); - this.syncCompleted(true); - return true; - } - return this.syncCompleted(false); - } - - async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - try { - let shouldUpdate = true; - const localCipher = await this.cipherService.get(notification.id); - if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { - shouldUpdate = false; - } - - let checkCollections = false; - if (shouldUpdate) { - if (isEdit) { - shouldUpdate = localCipher != null; - checkCollections = true; - } else { - if (notification.collectionIds == null || notification.organizationId == null) { - shouldUpdate = localCipher == null; - } else { - shouldUpdate = false; - checkCollections = true; - } - } - } - - if ( - !shouldUpdate && - checkCollections && - notification.organizationId != null && - notification.collectionIds != null && - notification.collectionIds.length > 0 - ) { - const collections = await this.collectionService.getAll(); - if (collections != null) { - for (let i = 0; i < collections.length; i++) { - if (notification.collectionIds.indexOf(collections[i].id) > -1) { - shouldUpdate = true; - break; - } - } - } - } - - if (shouldUpdate) { - const remoteCipher = await this.apiService.getFullCipherDetails(notification.id); - if (remoteCipher != null) { - await this.cipherService.upsert(new CipherData(remoteCipher)); - this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - if (e != null && e.statusCode === 404 && isEdit) { - await this.cipherService.delete(notification.id); - this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id }); - return this.syncCompleted(true); - } - } - } - return this.syncCompleted(false); - } - - async syncDeleteCipher(notification: SyncCipherNotification): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.cipherService.delete(notification.id); - this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id }); - return this.syncCompleted(true); - } - return this.syncCompleted(false); - } - - async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { - this.syncStarted(); - const [activeUserId, status] = await firstValueFrom( - this.accountService.activeAccount$.pipe( - switchMap((a) => { - if (a == null) { - of([null, AuthenticationStatus.LoggedOut]); - } - return this.authService.authStatusFor$(a.id).pipe(map((s) => [a.id, s])); - }), - ), - ); - // Process only notifications for currently active user when user is not logged out - // TODO: once send service allows data manipulation of non-active users, this should process any received notification - if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) { - try { - const localSend = await firstValueFrom(this.sendService.get$(notification.id)); - if ( - (!isEdit && localSend == null) || - (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) - ) { - const remoteSend = await this.sendApiService.getSend(notification.id); - if (remoteSend != null) { - await this.sendService.upsert(new SendData(remoteSend)); - this.messagingService.send("syncedUpsertedSend", { sendId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - this.logService.error(e); - } - } - return this.syncCompleted(false); - } - - async syncDeleteSend(notification: SyncSendNotification): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.sendService.delete(notification.id); - this.messagingService.send("syncedDeletedSend", { sendId: notification.id }); - this.syncCompleted(true); - return true; - } - return this.syncCompleted(false); - } - - // Helpers - - private syncStarted() { - this.syncInProgress = true; - this.messagingService.send("syncStarted"); - } - - private syncCompleted(successfully: boolean): boolean { - this.syncInProgress = false; - this.messagingService.send("syncCompleted", { successfully: successfully }); - return successfully; - } - private async needsSyncing(forceSync: boolean) { if (forceSync) { return true; @@ -365,7 +192,7 @@ export class SyncService implements SyncServiceAbstraction { if (await this.keyConnectorService.userNeedsMigration()) { await this.keyConnectorService.setConvertAccountRequired(true); - this.messagingService.send("convertAccountToKeyConnector"); + this.messageSender.send("convertAccountToKeyConnector"); } else { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises