diff options
Diffstat (limited to 'src/vs/platform/userDataSync')
6 files changed, 441 insertions, 12 deletions
diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 8371dabb50d..1f7f2860565 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -28,7 +28,7 @@ import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { ALL_SYNC_RESOURCES, Change, createSyncHeaders, getEnablementKey, IGlobalState, IRemoteUserData, IStorageValue, ISyncData, ISyncResourceHandle, IUserData, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, SYNC_SERVICE_URL_TYPE, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreType, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IProfileStorageService } from 'vs/platform/storage/common/profileStorageService'; +import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; const argvStoragePrefx = 'globalState.argv.'; const argvProperties: string[] = ['locale']; @@ -77,7 +77,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs constructor( private readonly profile: IUserDataProfile, - @IProfileStorageService private readonly profileStorageService: IProfileStorageService, + @IUserDataSyncProfilesStorageService private readonly userDataSyncProfilesStorageService: IUserDataSyncProfilesStorageService, @IFileService fileService: IFileService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @@ -95,7 +95,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs Event.any( /* Locale change */ Event.filter(fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)), - Event.filter(this.profileStorageService.onDidChange, e => { + Event.filter(this.userDataSyncProfilesStorageService.onDidChange, e => { /* StorageTarget has changed in profile storage */ if (e.targetChanges.some(profile => this.profile.id === profile.id)) { return true; @@ -311,7 +311,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] }; } } - const storageData = await this.profileStorageService.readStorageData(this.profile); + const storageData = await this.userDataSyncProfilesStorageService.readStorageData(this.profile); for (const [key, value] of storageData) { if (value.value && value.target === StorageTarget.USER) { storage[key] = { version: 1, value: value.value }; @@ -335,7 +335,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs private async writeLocalGlobalState({ added, removed, updated }: { added: IStringDictionary<IStorageValue>; updated: IStringDictionary<IStorageValue>; removed: string[] }): Promise<void> { const argv: IStringDictionary<any> = {}; const updatedStorage = new Map<string, string | undefined>(); - const storageData = await this.profileStorageService.readStorageData(this.profile); + const storageData = await this.userDataSyncProfilesStorageService.readStorageData(this.profile); const handleUpdatedStorage = (keys: string[], storage?: IStringDictionary<IStorageValue>): void => { for (const key of keys) { if (key.startsWith(argvStoragePrefx)) { @@ -364,7 +364,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } if (updatedStorage.size) { this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`); - await this.profileStorageService.updateStorageData(this.profile, updatedStorage, StorageTarget.USER); + await this.userDataSyncProfilesStorageService.updateStorageData(this.profile, updatedStorage, StorageTarget.USER); this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, [...updatedStorage.keys()]); } } @@ -383,7 +383,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } private async getStorageKeys(lastSyncGlobalState: IGlobalState | null): Promise<StorageKeys> { - const storageData = await this.profileStorageService.readStorageData(this.profile); + const storageData = await this.userDataSyncProfilesStorageService.readStorageData(this.profile); const user: string[] = [], machine: string[] = []; for (const [key, value] of storageData) { if (value.target === StorageTarget.USER) { diff --git a/src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts b/src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts new file mode 100644 index 00000000000..f79a4f4a894 --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { Disposable, isDisposable } from 'vs/base/common/lifecycle'; +import { IStorage, IStorageDatabase, Storage } from 'vs/base/parts/storage/common/storage'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { AbstractStorageService, IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; + +export interface IProfileStorageValueChanges { + readonly profile: IUserDataProfile; + readonly changes: IStorageValueChangeEvent[]; +} + +export interface IProfileStorageChanges { + readonly targetChanges: IUserDataProfile[]; + readonly valueChanges: IProfileStorageValueChanges[]; +} + +export interface IStorageValue { + readonly value: string | undefined; + readonly target: StorageTarget; +} + +export const IUserDataSyncProfilesStorageService = createDecorator<IUserDataSyncProfilesStorageService>('IUserDataSyncProfilesStorageService'); +export interface IUserDataSyncProfilesStorageService { + readonly _serviceBrand: undefined; + + /** + * Emitted whenever data is updated or deleted in a profile storage or target of a profile storage entry changes + */ + readonly onDidChange: Event<IProfileStorageChanges>; + + /** + * Return the requested profile storage data + * @param profile The profile from which the data has to be read from + */ + readStorageData(profile: IUserDataProfile): Promise<Map<string, IStorageValue>>; + + /** + * Update the given profile storage data in the profile storage + * @param profile The profile to which the data has to be written to + * @param data Data that has to be updated + * @param target Storage target of the data + */ + updateStorageData(profile: IUserDataProfile, data: Map<string, string | undefined | null>, target: StorageTarget): Promise<void>; +} + +export abstract class AbstractUserDataSyncProfilesStorageService extends Disposable implements IUserDataSyncProfilesStorageService { + + _serviceBrand: undefined; + + readonly abstract onDidChange: Event<IProfileStorageChanges>; + + constructor( + @IStorageService protected readonly storageService: IStorageService + ) { + super(); + } + + async readStorageData(profile: IUserDataProfile): Promise<Map<string, IStorageValue>> { + // Use current storage service if the profile is same + if (this.storageService.isProfileStorageFor(profile)) { + return this.getItems(this.storageService); + } + + const storageDatabase = await this.createStorageDatabase(profile); + const storageService = new StorageService(storageDatabase); + try { + await storageService.initialize(); + return this.getItems(storageService); + } finally { + storageService.dispose(); + await this.closeAndDispose(storageDatabase); + } + } + + async updateStorageData(profile: IUserDataProfile, data: Map<string, string | undefined | null>, target: StorageTarget): Promise<void> { + // Use current storage service if the profile is same + if (this.storageService.isProfileStorageFor(profile)) { + return this.writeItems(this.storageService, data, target); + } + + const storageDatabase = await this.createStorageDatabase(profile); + const storageService = new StorageService(storageDatabase); + try { + await storageService.initialize(); + this.writeItems(storageService, data, target); + await storageService.flush(); + } finally { + storageService.dispose(); + await this.closeAndDispose(storageDatabase); + } + } + + private getItems(storageService: IStorageService): Map<string, IStorageValue> { + const result = new Map<string, IStorageValue>(); + const populate = (target: StorageTarget) => { + for (const key of storageService.keys(StorageScope.PROFILE, target)) { + result.set(key, { value: storageService.get(key, StorageScope.PROFILE), target }); + } + }; + populate(StorageTarget.USER); + populate(StorageTarget.MACHINE); + return result; + } + + private writeItems(storageService: IStorageService, items: Map<string, string | undefined | null>, target: StorageTarget): void { + for (const [key, value] of items) { + storageService.store(key, value, StorageScope.PROFILE, target); + } + } + + protected async closeAndDispose(storageDatabase: IStorageDatabase): Promise<void> { + try { + await storageDatabase.close(); + } finally { + if (isDisposable(storageDatabase)) { + storageDatabase.dispose(); + } + } + } + + protected abstract createStorageDatabase(profile: IUserDataProfile): Promise<IStorageDatabase>; +} + +class StorageService extends AbstractStorageService { + + private readonly profileStorage: IStorage; + + constructor(profileStorageDatabase: IStorageDatabase) { + super({ flushInterval: 100, doNotMarkPerf: true }); + this.profileStorage = this._register(new Storage(profileStorageDatabase)); + } + + protected doInitialize(): Promise<void> { + return this.profileStorage.init(); + } + + protected getStorage(scope: StorageScope): IStorage | undefined { + return scope === StorageScope.PROFILE ? this.profileStorage : undefined; + } + + protected getLogDetails(): string | undefined { return undefined; } + protected async switchToProfile(): Promise<void> { } + protected async switchToWorkspace(): Promise<void> { } + isProfileStorageFor() { return false; } +} diff --git a/src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts b/src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts new file mode 100644 index 00000000000..931cc8d69b4 --- /dev/null +++ b/src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProfileStorageChanges, IProfileStorageValueChanges } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { loadKeyTargets, StorageScope, TARGET_KEY } from 'vs/platform/storage/common/storage'; +import { IBaseSerializableStorageRequest } from 'vs/platform/storage/common/storageIpc'; +import { IStorageMain } from 'vs/platform/storage/electron-main/storageMain'; +import { IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; + +export class ProfileStorageChangesListenerChannel extends Disposable implements IServerChannel { + + private readonly _onDidChange: Emitter<IProfileStorageChanges>; + + constructor( + private readonly storageMainService: IStorageMainService, + private readonly userDataProfilesService: IUserDataProfilesService, + private readonly logService: ILogService + ) { + super(); + const disposable = this._register(new MutableDisposable<IDisposable>()); + this._onDidChange = this._register(new Emitter<IProfileStorageChanges>( + { + // Start listening to profile storage changes only when someone is listening + onFirstListenerAdd: () => disposable.value = this.registerStorageChangeListeners(), + // Stop listening to profile storage changes when no one is listening + onLastListenerRemove: () => disposable.value = undefined + } + )); + } + + private registerStorageChangeListeners(): IDisposable { + this.logService.debug('ProfileStorageChangesListenerChannel#registerStorageChangeListeners'); + const disposables = new DisposableStore(); + disposables.add(Event.debounce(this.storageMainService.applicationStorage.onDidChangeStorage, (keys: string[] | undefined, e) => { + if (keys) { + keys.push(e.key); + } else { + keys = [e.key]; + } + return keys; + }, 100)(keys => this.onDidChangeApplicationStorage(keys))); + disposables.add(Event.debounce(this.storageMainService.onDidChangeProfileStorageData, (changes: Map<string, { profile: IUserDataProfile; keys: string[]; storage: IStorageMain }> | undefined, e) => { + if (!changes) { + changes = new Map<string, { profile: IUserDataProfile; keys: string[]; storage: IStorageMain }>(); + } + let profileChanges = changes.get(e.profile.id); + if (!profileChanges) { + changes.set(e.profile.id, profileChanges = { profile: e.profile, keys: [], storage: e.storage }); + } + profileChanges.keys.push(e.key); + return changes; + }, 100)(keys => this.onDidChangeProfileStorage(keys))); + return disposables; + } + + private onDidChangeApplicationStorage(keys: string[]): void { + const targetChangedProfiles: IUserDataProfile[] = keys.includes(TARGET_KEY) ? [this.userDataProfilesService.defaultProfile] : []; + const profileStorageValueChanges: IProfileStorageValueChanges[] = []; + keys = keys.filter(key => key !== TARGET_KEY); + if (keys.length) { + const keyTargets = loadKeyTargets(this.storageMainService.applicationStorage.storage); + profileStorageValueChanges.push({ profile: this.userDataProfilesService.defaultProfile, changes: keys.map(key => ({ key, scope: StorageScope.PROFILE, target: keyTargets[key] })) }); + } + this.triggerEvents(targetChangedProfiles, profileStorageValueChanges); + } + + private onDidChangeProfileStorage(changes: Map<string, { profile: IUserDataProfile; keys: string[]; storage: IStorageMain }>): void { + const targetChangedProfiles: IUserDataProfile[] = []; + const profileStorageValueChanges = new Map<string, IProfileStorageValueChanges>(); + for (const [profileId, profileChanges] of changes.entries()) { + if (profileChanges.keys.includes(TARGET_KEY)) { + targetChangedProfiles.push(profileChanges.profile); + } + const keys = profileChanges.keys.filter(key => key !== TARGET_KEY); + if (keys.length) { + const keyTargets = loadKeyTargets(profileChanges.storage.storage); + profileStorageValueChanges.set(profileId, { profile: profileChanges.profile, changes: keys.map(key => ({ key, scope: StorageScope.PROFILE, target: keyTargets[key] })) }); + } + } + this.triggerEvents(targetChangedProfiles, [...profileStorageValueChanges.values()]); + } + + private triggerEvents(targetChanges: IUserDataProfile[], valueChanges: IProfileStorageValueChanges[]): void { + if (targetChanges.length || valueChanges.length) { + this._onDidChange.fire({ valueChanges, targetChanges }); + } + } + + listen(_: unknown, event: string, arg: IBaseSerializableStorageRequest): Event<any> { + switch (event) { + case 'onDidChange': return this._onDidChange.event; + } + throw new Error(`Event not found: ${event}`); + } + + async call(_: unknown, command: string): Promise<any> { + throw new Error(`Call not found: ${command}`); + } + +} diff --git a/src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts b/src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts new file mode 100644 index 00000000000..c668af40dfd --- /dev/null +++ b/src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; +import { IStorageDatabase } from 'vs/base/parts/storage/common/storage'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractUserDataSyncProfilesStorageService, IProfileStorageChanges, IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { isProfileUsingDefaultStorage, IStorageService } from 'vs/platform/storage/common/storage'; +import { ApplicationStorageDatabaseClient, ProfileStorageDatabaseClient } from 'vs/platform/storage/common/storageIpc'; +import { IUserDataProfile, IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; + +export class UserDataSyncProfilesStorageService extends AbstractUserDataSyncProfilesStorageService implements IUserDataSyncProfilesStorageService { + + private readonly _onDidChange: Emitter<IProfileStorageChanges>; + readonly onDidChange: Event<IProfileStorageChanges>; + + constructor( + @IMainProcessService private readonly mainProcessService: IMainProcessService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IStorageService storageService: IStorageService, + @ILogService logService: ILogService, + ) { + super(storageService); + + const channel = mainProcessService.getChannel('profileStorageListener'); + const disposable = this._register(new MutableDisposable()); + this._onDidChange = this._register(new Emitter<IProfileStorageChanges>({ + // Start listening to profile storage changes only when someone is listening + onFirstListenerAdd: () => { + disposable.value = channel.listen<IProfileStorageChanges>('onDidChange')(e => { + logService.trace('profile storage changes', e); + this._onDidChange.fire({ + targetChanges: e.targetChanges.map(profile => reviveProfile(profile, userDataProfilesService.profilesHome.scheme)), + valueChanges: e.valueChanges.map(e => ({ ...e, profile: reviveProfile(e.profile, userDataProfilesService.profilesHome.scheme) })) + }); + }); + }, + // Stop listening to profile storage changes when no one is listening + onLastListenerRemove: () => disposable.value = undefined + })); + this.onDidChange = this._onDidChange.event; + } + + protected async createStorageDatabase(profile: IUserDataProfile): Promise<IStorageDatabase> { + const storageChannel = this.mainProcessService.getChannel('storage'); + return isProfileUsingDefaultStorage(profile) ? new ApplicationStorageDatabaseClient(storageChannel) : new ProfileStorageDatabaseClient(storageChannel, profile); + } +} diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 4bcf4b48db3..c81de3f5134 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -43,8 +43,8 @@ import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfile, IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { NullPolicyService } from 'vs/platform/policy/common/policy'; -import { IProfileStorageService } from 'vs/platform/storage/common/profileStorageService'; -import { TestProfileStorageService } from 'vs/platform/storage/test/common/profileStorageService.test'; +import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { TestUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test'; export class UserDataSyncClient extends Disposable { @@ -92,7 +92,7 @@ export class UserDataSyncClient extends Disposable { const storageService = new TestStorageService(userDataProfilesService.defaultProfile); this.instantiationService.stub(IStorageService, this._register(storageService)); - this.instantiationService.stub(IProfileStorageService, this._register(new TestProfileStorageService(storageService))); + this.instantiationService.stub(IUserDataSyncProfilesStorageService, this._register(new TestUserDataSyncProfilesStorageService(storageService))); const configurationService = this._register(new ConfigurationService(userDataProfilesService.defaultProfile.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); @@ -307,7 +307,7 @@ class TestStorageService extends InMemoryStorageService { constructor(private readonly profileStorageProfile: IUserDataProfile) { super(); } - override getProfileStorageProfile(): IUserDataProfile | undefined { - return this.profileStorageProfile; + override isProfileStorageFor(profile: IUserDataProfile): boolean { + return this.profileStorageProfile.id === profile.id; } } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts new file mode 100644 index 00000000000..fe4c35019ef --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { InMemoryStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; +import { AbstractUserDataSyncProfilesStorageService, IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { InMemoryStorageService, loadKeyTargets, StorageTarget, TARGET_KEY } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; + +class TestStorageDatabase extends InMemoryStorageDatabase { + + private readonly _onDidChangeItemsExternal = new Emitter<IStorageItemsChangeEvent>(); + override readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; + + override async updateItems(request: IUpdateRequest): Promise<void> { + await super.updateItems(request); + if (request.insert || request.delete) { + this._onDidChangeItemsExternal.fire({ changed: request.insert, deleted: request.delete }); + } + } +} + +export class TestUserDataSyncProfilesStorageService extends AbstractUserDataSyncProfilesStorageService implements IUserDataSyncProfilesStorageService { + + readonly onDidChange = Event.None; + private databases = new Map<string, InMemoryStorageDatabase>(); + + async createStorageDatabase(profile: IUserDataProfile): Promise<InMemoryStorageDatabase> { + let database = this.databases.get(profile.id); + if (!database) { + this.databases.set(profile.id, database = new TestStorageDatabase()); + } + return database; + } + + protected override async closeAndDispose(): Promise<void> { } +} + +suite('ProfileStorageService', () => { + + const disposables = new DisposableStore(); + const profile = toUserDataProfile('test', URI.file('foo')); + let testObject: TestUserDataSyncProfilesStorageService; + let storage: Storage; + + setup(async () => { + testObject = disposables.add(new TestUserDataSyncProfilesStorageService(new InMemoryStorageService())); + storage = new Storage(await testObject.createStorageDatabase(profile)); + await storage.init(); + }); + + teardown(() => disposables.clear()); + + test('read empty storage', async () => { + const actual = await testObject.readStorageData(profile); + + assert.strictEqual(actual.size, 0); + }); + + test('read storage with data', async () => { + storage.set('foo', 'bar'); + storage.set(TARGET_KEY, JSON.stringify({ foo: StorageTarget.USER })); + await storage.flush(); + + const actual = await testObject.readStorageData(profile); + + assert.strictEqual(actual.size, 1); + assert.deepStrictEqual(actual.get('foo'), { 'value': 'bar', 'target': StorageTarget.USER }); + }); + + test('write in empty storage', async () => { + const data = new Map<string, string>(); + data.set('foo', 'bar'); + await testObject.updateStorageData(profile, data, StorageTarget.USER); + + assert.strictEqual(storage.items.size, 2); + assert.deepStrictEqual(loadKeyTargets(storage), { foo: StorageTarget.USER }); + assert.strictEqual(storage.get('foo'), 'bar'); + }); + + test('write in storage with data', async () => { + storage.set('foo', 'bar'); + storage.set(TARGET_KEY, JSON.stringify({ foo: StorageTarget.USER })); + await storage.flush(); + + const data = new Map<string, string>(); + data.set('abc', 'xyz'); + await testObject.updateStorageData(profile, data, StorageTarget.MACHINE); + + assert.strictEqual(storage.items.size, 3); + assert.deepStrictEqual(loadKeyTargets(storage), { foo: StorageTarget.USER, abc: StorageTarget.MACHINE }); + assert.strictEqual(storage.get('foo'), 'bar'); + assert.strictEqual(storage.get('abc'), 'xyz'); + }); + + test('write in storage with data (insert, update, remove)', async () => { + storage.set('foo', 'bar'); + storage.set('abc', 'xyz'); + storage.set(TARGET_KEY, JSON.stringify({ foo: StorageTarget.USER, abc: StorageTarget.MACHINE })); + await storage.flush(); + + const data = new Map<string, string | undefined>(); + data.set('foo', undefined); + data.set('abc', 'def'); + data.set('var', 'const'); + await testObject.updateStorageData(profile, data, StorageTarget.USER); + + assert.strictEqual(storage.items.size, 3); + assert.deepStrictEqual(loadKeyTargets(storage), { abc: StorageTarget.USER, var: StorageTarget.USER }); + assert.strictEqual(storage.get('abc'), 'def'); + assert.strictEqual(storage.get('var'), 'const'); + }); + +}); |