diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 70978f40704..b1be350f49b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -849,6 +849,7 @@ export default class MainBackground { this.sendService, this.sendApiService, messageListener, + this.stateProvider, ); } else { this.syncService = new DefaultSyncService( @@ -876,6 +877,7 @@ export default class MainBackground { this.billingAccountProfileStateService, this.tokenService, this.authService, + this.stateProvider, ); this.syncServiceListener = new SyncServiceListener( @@ -1358,7 +1360,6 @@ export default class MainBackground { ); await Promise.all([ - this.syncService.setLastSync(new Date(0), userBeingLoggedOut), this.cryptoService.clearKeys(userBeingLoggedOut), this.cipherService.clear(userBeingLoggedOut), this.folderService.clear(userBeingLoggedOut), diff --git a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts index a9ee7c23b9c..365ce6a83ca 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts @@ -7,8 +7,11 @@ 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 { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { UserId } from "@bitwarden/common/types/guid"; 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"; @@ -18,6 +21,7 @@ import { DO_FULL_SYNC, ForegroundSyncService, FullSyncMessage } from "./foregrou import { FullSyncFinishedMessage } from "./sync-service.listener"; describe("ForegroundSyncService", () => { + const userId = Utils.newGuid() as UserId; const stateService = mock(); const folderService = mock(); const folderApiService = mock(); @@ -31,6 +35,7 @@ describe("ForegroundSyncService", () => { const sendService = mock(); const sendApiService = mock(); const messageListener = mock(); + const stateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); const sut = new ForegroundSyncService( stateService, @@ -46,6 +51,7 @@ describe("ForegroundSyncService", () => { sendService, sendApiService, messageListener, + stateProvider, ); beforeEach(() => { diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts index 0a2c7074298..23c0e1ff9f9 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -11,6 +11,7 @@ import { MessageSender, } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; 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"; @@ -40,6 +41,7 @@ export class ForegroundSyncService extends CoreSyncService { sendService: InternalSendService, sendApiService: SendApiService, private readonly messageListener: MessageListener, + stateProvider: StateProvider, ) { super( stateService, @@ -54,6 +56,7 @@ export class ForegroundSyncService extends CoreSyncService { authService, sendService, sendApiService, + stateProvider, ); } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index d35412ede30..9cb70952593 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -699,6 +699,7 @@ export class ServiceContainer { this.billingAccountProfileStateService, this.tokenService, this.authService, + this.stateProvider, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); @@ -772,7 +773,6 @@ export class ServiceContainer { const userId = (await this.stateService.getUserId()) as UserId; await Promise.all([ this.eventUploadService.uploadEvents(userId as UserId), - this.syncService.setLastSync(new Date(0)), this.cryptoService.clearKeys(), this.cipherService.clear(userId), this.folderService.clear(userId), diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index a6499f6f82b..a311ed2b865 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -650,7 +650,6 @@ export class AppComponent implements OnInit, OnDestroy { // Provide the userId of the user to upload events for await this.eventUploadService.uploadEvents(userBeingLoggedOut); - await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut); await this.folderService.clear(userBeingLoggedOut); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 1c5527504d9..b05c82226ab 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -323,7 +323,6 @@ export class AppComponent implements OnDestroy, OnInit { ); await Promise.all([ - this.syncService.setLastSync(new Date(0)), this.cryptoService.clearKeys(), this.cipherService.clear(userId), this.folderService.clear(userId), diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 2ff81c02b4e..d0a4376556a 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -26,11 +26,12 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../../utils/flags"; -import { RouterService, StateService } from "../../core"; +import { RouterService } from "../../core"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; import { OrganizationInvite } from "../organization-invite/organization-invite"; diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 45cfa8a3552..4d8acbde0d1 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -38,7 +38,6 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; @@ -71,7 +70,6 @@ import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ModalService } from "./modal.service"; import { RouterService } from "./router.service"; -import { StateService as WebStateService } from "./state"; import { WebFileDownloadService } from "./web-file-download.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; @@ -135,11 +133,6 @@ const safeProviders: SafeProvider[] = [ useClass: ModalService, useAngularDecorators: true, }), - safeProvider(WebStateService), - safeProvider({ - provide: StateService, - useExisting: WebStateService, - }), safeProvider({ provide: FileDownloadService, useClass: WebFileDownloadService, diff --git a/apps/web/src/app/core/index.ts b/apps/web/src/app/core/index.ts index f2a058eeb3c..abbbfd2231a 100644 --- a/apps/web/src/app/core/index.ts +++ b/apps/web/src/app/core/index.ts @@ -1,4 +1,3 @@ export * from "./core.module"; export * from "./event.service"; export * from "./router.service"; -export * from "./state/state.service"; diff --git a/apps/web/src/app/core/state/index.ts b/apps/web/src/app/core/state/index.ts deleted file mode 100644 index 4fa4d160ac8..00000000000 --- a/apps/web/src/app/core/state/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./state.service"; diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts deleted file mode 100644 index c60698acf60..00000000000 --- a/apps/web/src/app/core/state/state.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Inject, Injectable } from "@angular/core"; - -import { - MEMORY_STORAGE, - SECURE_STORAGE, - STATE_FACTORY, -} from "@bitwarden/angular/services/injection-tokens"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; -import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; - -@Injectable() -export class StateService extends BaseStateService { - constructor( - storageService: AbstractStorageService, - @Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService, - @Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService, - logService: LogService, - @Inject(STATE_FACTORY) stateFactory: StateFactory, - accountService: AccountService, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) { - super( - storageService, - secureStorageService, - memoryStorageService, - logService, - stateFactory, - accountService, - environmentService, - tokenService, - migrationRunner, - ); - } - - override async getLastSync(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.getLastSync(options); - } - - override async setLastSync(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.setLastSync(value, options); - } -} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index db1eb1a577f..c8b6011c815 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -685,6 +685,7 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, TokenServiceAbstraction, AuthServiceAbstraction, + StateProvider, ], }), safeProvider({ diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index d9498905bbe..1ffe5b4353e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -59,7 +59,5 @@ export abstract class StateService { getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; - getLastSync: (options?: StorageOptions) => Promise; - setLastSync: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; } diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index b367d617dac..199b99a8e7e 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -95,7 +95,6 @@ export class AccountProfile { name?: string; email?: string; emailVerified?: boolean; - lastSync?: string; userId?: string; static fromJSON(obj: Jsonify): AccountProfile { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index c6e01bf00a1..70d2211a884 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -301,23 +301,6 @@ export class StateService< ); } - async getLastSync(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.profile?.lastSync; - } - - async setLastSync(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.profile.lastSync = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index fb00942f3e3..f7609e877d8 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -110,6 +110,7 @@ export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory"); export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk"); export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk"); export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory"); +export const SYNC_DISK = new StateDefinition("sync", "disk", { web: "memory" }); export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" }); export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" }); export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk"); diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 52c1a51cb82..fd028df09b0 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -12,6 +12,7 @@ import { 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 { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CollectionService } from "../../vault/abstractions/collection.service"; import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction"; @@ -22,6 +23,12 @@ import { FolderData } from "../../vault/models/data/folder.data"; import { LogService } from "../abstractions/log.service"; import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; +import { StateProvider, SYNC_DISK, UserKeyDefinition } from "../state"; + +const LAST_SYNC_DATE = new UserKeyDefinition(SYNC_DISK, "lastSync", { + deserializer: (d) => (d != null ? new Date(d) : null), + clearOn: ["logout"], +}); /** * Core SyncService Logic EXCEPT for fullSync so that implementations can differ. @@ -42,25 +49,26 @@ export abstract class CoreSyncService implements SyncService { protected readonly authService: AuthService, protected readonly sendService: InternalSendService, protected readonly sendApiService: SendApiService, + protected readonly stateProvider: StateProvider, ) {} abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; async getLastSync(): Promise { - if ((await this.stateService.getUserId()) == null) { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + if (userId == null) { return null; } - const lastSync = await this.stateService.getLastSync(); - if (lastSync) { - return new Date(lastSync); - } - - return null; + return await firstValueFrom(this.lastSync$(userId)); } - async setLastSync(date: Date, userId?: string): Promise { - await this.stateService.setLastSync(date.toJSON(), { userId: userId }); + lastSync$(userId: UserId) { + return this.stateProvider.getUser(userId, LAST_SYNC_DATE).state$; + } + + async setLastSync(date: Date, userId: UserId): Promise { + await this.stateProvider.getUser(userId, LAST_SYNC_DATE).update(() => date); } async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 5058288487f..6877f5a1ca5 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { LogoutReason } from "../../../../auth/src/common/types"; @@ -17,6 +17,7 @@ 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"; @@ -42,6 +43,7 @@ import { LogService } from "../abstractions/log.service"; import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; import { sequentialize } from "../misc/sequentialize"; +import { StateProvider } from "../state"; import { CoreSyncService } from "./core-sync.service"; @@ -73,6 +75,7 @@ export class DefaultSyncService extends CoreSyncService { private billingAccountProfileStateService: BillingAccountProfileStateService, private tokenService: TokenService, authService: AuthService, + stateProvider: StateProvider, ) { super( stateService, @@ -87,14 +90,16 @@ export class DefaultSyncService extends CoreSyncService { authService, sendService, sendApiService, + stateProvider, ); } @sequentialize(() => "fullSync") override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); this.syncStarted(); - const isAuthenticated = await this.stateService.getIsAuthenticated(); - if (!isAuthenticated) { + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); + if (authStatus === AuthenticationStatus.LoggedOut) { return this.syncCompleted(false); } @@ -110,7 +115,7 @@ export class DefaultSyncService extends CoreSyncService { } if (!needsSync) { - await this.setLastSync(now); + await this.setLastSync(now, userId); return this.syncCompleted(false); } @@ -126,7 +131,7 @@ export class DefaultSyncService extends CoreSyncService { await this.syncSettings(response.domains); await this.syncPolicies(response.policies); - await this.setLastSync(now); + await this.setLastSync(now, userId); return this.syncCompleted(true); } catch (e) { if (allowThrowOnError) { diff --git a/libs/common/src/platform/sync/sync.service.ts b/libs/common/src/platform/sync/sync.service.ts index 741657d5353..be5aa4622c6 100644 --- a/libs/common/src/platform/sync/sync.service.ts +++ b/libs/common/src/platform/sync/sync.service.ts @@ -1,8 +1,11 @@ +import { Observable } from "rxjs"; + import { SyncCipherNotification, SyncFolderNotification, SyncSendNotification, } from "../../models/response/notification.response"; +import { UserId } from "../../types/guid"; /** * A class encapsulating sync operations and data. @@ -20,15 +23,16 @@ export abstract class SyncService { * Gets the date of the last sync for the currently active user. * * @returns The date of the last sync or null if there is no active user or the active user has not synced before. + * + * @deprecated Use {@link lastSync$} to get an observable stream of a given users last sync date instead. */ - abstract getLastSync(): Promise; + abstract getLastSync(): Promise; /** - * Updates a users last sync date. - * @param date The date to be set as the users last sync date. - * @param userId The userId of the user to update the last sync date for. + * Retrieves a stream of the given users last sync date. Or null if the user has not synced before. + * @param userId The user id of the user to get the stream for. */ - abstract setLastSync(date: Date, userId?: string): Promise; + abstract lastSync$(userId: UserId): Observable; /** * Optionally does a full sync operation including going to the server to gather the source diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 3d849cfbf77..81b1016a53d 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -64,13 +64,15 @@ import { PasswordOptionsMigrator } from "./migrations/63-migrate-password-settin import { GeneratorHistoryMigrator } from "./migrations/64-migrate-generator-history"; import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-settings"; import { MoveFinalDesktopSettingsMigrator } from "./migrations/66-move-final-desktop-settings"; +import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-unassigned-items-banner-dismissed"; +import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 66; +export const CURRENT_VERSION = 68; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -138,7 +140,9 @@ export function createMigrationBuilder() { .with(PasswordOptionsMigrator, 62, 63) .with(GeneratorHistoryMigrator, 63, 64) .with(ForwarderOptionsMigrator, 64, 65) - .with(MoveFinalDesktopSettingsMigrator, 65, CURRENT_VERSION); + .with(MoveFinalDesktopSettingsMigrator, 65, 66) + .with(RemoveUnassignedItemsBannerDismissed, 66, 67) + .with(MoveLastSyncDate, 67, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/68-move-last-sync-date.spec.ts b/libs/common/src/state-migrations/migrations/68-move-last-sync-date.spec.ts new file mode 100644 index 00000000000..9d298f16103 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/68-move-last-sync-date.spec.ts @@ -0,0 +1,91 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MoveLastSyncDate } from "./68-move-last-sync-date"; + +describe("MoveLastSyncDate", () => { + const sut = new MoveLastSyncDate(67, 68); + it("migrates data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: null, + user2: null, + user3: null, + user4: null, + user5: null, + }, + user1: { + profile: { + lastSync: "2024-07-24T14:27:25.703Z", + }, + }, + user2: {}, + user3: { profile: null }, + user4: { profile: {} }, + user5: { profile: { lastSync: null } }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + user3: null, + user4: null, + user5: null, + }, + user1: { + profile: {}, + }, + user2: {}, + user3: { profile: null }, + user4: { profile: {} }, + user5: { profile: { lastSync: null } }, + user_user1_sync_lastSync: "2024-07-24T14:27:25.703Z", + }); + }); + + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: null, + user2: null, + user3: null, + user4: null, + user5: null, + }, + user1: { + profile: { + extraProperty: "hello", + }, + }, + user2: {}, + user3: { profile: null }, + user4: { profile: {} }, + user5: { profile: { lastSync: null } }, + user_user1_sync_lastSync: "2024-07-24T14:27:25.703Z", + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + user3: null, + user4: null, + user5: null, + }, + user1: { + profile: { + lastSync: "2024-07-24T14:27:25.703Z", + extraProperty: "hello", + }, + }, + user2: {}, + user3: { profile: null }, + user4: { profile: {} }, + user5: { profile: { lastSync: null } }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/68-move-last-sync-date.ts b/libs/common/src/state-migrations/migrations/68-move-last-sync-date.ts new file mode 100644 index 00000000000..b8957b0d241 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/68-move-last-sync-date.ts @@ -0,0 +1,49 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccount = { + profile?: { + lastSync?: string; + }; +}; + +const LAST_SYNC_KEY: KeyDefinitionLike = { + key: "lastSync", + stateDefinition: { + name: "sync", + }, +}; + +export class MoveLastSyncDate extends Migrator<67, 68> { + async migrate(helper: MigrationHelper): Promise { + async function migrateAccount(userId: string, account: ExpectedAccount) { + const value = account?.profile?.lastSync; + if (value != null) { + await helper.setToUser(userId, LAST_SYNC_KEY, value); + + delete account.profile.lastSync; + await helper.set(userId, account); + } + } + + const accounts = await helper.getAccounts(); + await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account))); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackAccount(userId: string, account: ExpectedAccount) { + const value = await helper.getFromUser(userId, LAST_SYNC_KEY); + + if (value != null) { + account ??= {}; + account.profile ??= {}; + account.profile.lastSync = value; + await helper.set(userId, account); + await helper.removeFromUser(userId, LAST_SYNC_KEY); + } + } + + const accounts = await helper.getAccounts(); + await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account))); + } +}