diff options
17 files changed, 637 insertions, 74 deletions
diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 375339be219..5638094882a 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -106,6 +106,8 @@ import { UserDataProfilesNativeService } from 'vs/platform/userDataProfile/elect import { SharedProcessRequestService } from 'vs/platform/request/electron-browser/sharedProcessRequestService'; import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender'; import { UserDataProfilesCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/userDataProfilesCleaner'; +import { UserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService'; +import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; class SharedProcessMain extends Disposable { @@ -338,6 +340,7 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService, undefined, false /* Eagerly cleans up old backups */)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService, undefined, true)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService, undefined, false /* Initializes the Sync State */)); + services.set(IUserDataSyncProfilesStorageService, new SyncDescriptor(UserDataSyncProfilesStorageService, undefined, true)); const ptyHostService = new PtyHostService({ graceTime: LocalReconnectConstants.GraceTime, diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d8820c5adb7..2e57bc75e9f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -108,6 +108,7 @@ import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } fro import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; import { UserDataTransientProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler'; +import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc'; import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; /** @@ -806,6 +807,10 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); + // Profile Storage Changes Listener (shared process) + const profileStorageListener = this._register(new ProfileStorageChangesListenerChannel(accessor.get(IStorageMainService), accessor.get(IUserDataProfilesMainService), this.logService)); + sharedProcessClient.then(client => client.registerChannel('profileStorageListener', profileStorageListener)); + // External Terminal const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService)); mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel); diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index e3c97f93cd2..0fb8aecd112 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -81,7 +81,8 @@ suite('FindController', async () => { flush: () => { return Promise.resolve(); }, keys: () => [], log: () => { }, - switch: () => { throw new Error(); } + switch: () => { throw new Error(); }, + isProfileStorageFor() { return false; } } as IStorageService); if (platform.isMacintosh) { @@ -512,7 +513,8 @@ suite('FindController query options persistence', async () => { flush: () => { return Promise.resolve(); }, keys: () => [], log: () => { }, - switch: () => { throw new Error(); } + switch: () => { throw new Error(); }, + isProfileStorageFor() { return false; } } as IStorageService); test('matchCase', async () => { diff --git a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts index 70f5e5a648d..a4fcd1f449d 100644 --- a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts @@ -98,7 +98,8 @@ suite('Multicursor selection', () => { switch: () => Promise.resolve(undefined), flush: () => Promise.resolve(undefined), isNew: () => true, - keys: () => [] + keys: () => [], + isProfileStorageFor() { return false; } } as IStorageService); test('issue #8817: Cursor position changes when you cancel multicursor', () => { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index de09d638262..0cc886a9fdf 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -14,7 +14,7 @@ import { isUserDataProfile, IUserDataProfile } from 'vs/platform/userDataProfile import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export const IS_NEW_KEY = '__$__isNewStorageMarker'; -const TARGET_KEY = '__$__targetStorageMarker'; +export const TARGET_KEY = '__$__targetStorageMarker'; export const IStorageService = createDecorator<IStorageService>('storageService'); @@ -141,6 +141,11 @@ export interface IStorageService { log(): void; /** + * Returns true if the given profile is used for profile storage + */ + isProfileStorageFor(profile: IUserDataProfile): boolean; + + /** * Switch storage to another workspace or profile. Optionally preserve the * current data to the new storage. */ @@ -230,7 +235,20 @@ interface IKeyTargets { } export interface IStorageServiceOptions { - flushInterval: number; + readonly flushInterval: number; + readonly doNotMarkPerf?: boolean; +} + +export function loadKeyTargets(storage: IStorage): IKeyTargets { + const keysRaw = storage.get(TARGET_KEY); + if (keysRaw) { + try { + return JSON.parse(keysRaw); + } catch (error) { + // Fail gracefully + } + } + return Object.create(null); } export abstract class AbstractStorageService extends Disposable implements IStorageService { @@ -280,13 +298,17 @@ export abstract class AbstractStorageService extends Disposable implements IStor if (!this.initializationPromise) { this.initializationPromise = (async () => { - // Init all storage locations - mark('code/willInitStorage'); + if (!this.options.doNotMarkPerf) { + // Init all storage locations + mark('code/willInitStorage'); + } try { // Ask subclasses to initialize storage await this.doInitialize(); } finally { - mark('code/didInitStorage'); + if (!this.options.doNotMarkPerf) { + mark('code/didInitStorage'); + } } // On some OS we do not get enough time to persist state on shutdown (e.g. when @@ -475,16 +497,8 @@ export abstract class AbstractStorageService extends Disposable implements IStor } private loadKeyTargets(scope: StorageScope): { [key: string]: StorageTarget } { - const keysRaw = this.get(TARGET_KEY, scope); - if (keysRaw) { - try { - return JSON.parse(keysRaw); - } catch (error) { - // Fail gracefully - } - } - - return Object.create(null); + const storage = this.getStorage(scope); + return storage ? loadKeyTargets(storage) : Object.create(null); } isNew(scope: StorageScope): boolean { @@ -603,6 +617,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor protected abstract switchToProfile(toProfile: IUserDataProfile, preserveData: boolean): Promise<void>; protected abstract switchToWorkspace(toWorkspace: IAnyWorkspaceIdentifier | IUserDataProfile, preserveData: boolean): Promise<void>; + abstract isProfileStorageFor(profile: IUserDataProfile): boolean; } export function isProfileUsingDefaultStorage(profile: IUserDataProfile): boolean { @@ -654,6 +669,10 @@ export class InMemoryStorageService extends AbstractStorageService { protected async switchToWorkspace(): Promise<void> { // no-op when in-memory } + + isProfileStorageFor(profiile: IUserDataProfile): boolean { + return false; + } } export async function logStorage(application: Map<string, string>, profile: Map<string, string>, workspace: Map<string, string>, applicationPath: string, profilePath: string, workspacePath: string): Promise<void> { diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 571a03c3cae..afdce9aa8a3 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -5,6 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { once } from 'vs/base/common/functional'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStorage } from 'vs/base/parts/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -13,7 +14,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractStorageService, isProfileUsingDefaultStorage, IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ApplicationStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain'; +import { ApplicationStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain, IStorageChangeEvent } from 'vs/platform/storage/electron-main/storageMain'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -37,6 +38,11 @@ export interface IStorageMainService { applicationStorage: IStorageMain; /** + * Emitted whenever data is updated or deleted in the profile storage. + */ + readonly onDidChangeProfileStorageData: Event<IStorageChangeEvent & { storage: IStorageMain; profile: IUserDataProfile }>; + + /** * Provides access to the profile storage shared across all windows * for the provided profile. * @@ -67,6 +73,9 @@ export class StorageMainService extends Disposable implements IStorageMainServic private shutdownReason: ShutdownReason | undefined = undefined; + private readonly _onDidChangeProfileStorageData = this._register(new Emitter<IStorageChangeEvent & { storage: IStorageMain; profile: IUserDataProfile }>()); + readonly onDidChangeProfileStorageData = this._onDidChangeProfileStorageData.event; + constructor( @ILogService private readonly logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @@ -180,6 +189,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic profileStorage = this.createProfileStorage(profile); this.mapProfileToStorage.set(profile.id, profileStorage); + this._register(profileStorage.onDidChangeStorage(e => this._onDidChangeProfileStorageData.fire({ ...e, storage: profileStorage!, profile }))); once(profileStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed profile storage (${profile.name})`); @@ -359,4 +369,8 @@ export class ApplicationStorageMainService extends AbstractStorageService implem protected switchToWorkspace(): never { throw new Error('Switching storage workspace is unsupported from main process'); } + + isProfileStorageFor(): never { + throw new Error('Profile storage is unsupported from main process'); + } } diff --git a/src/vs/platform/storage/electron-sandbox/storageService.ts b/src/vs/platform/storage/electron-sandbox/storageService.ts index d5bf2548f20..739030ae362 100644 --- a/src/vs/platform/storage/electron-sandbox/storageService.ts +++ b/src/vs/platform/storage/electron-sandbox/storageService.ts @@ -177,4 +177,8 @@ export class NativeStorageService extends AbstractStorageService { // Handle data switch and eventing this.switchData(oldItems, this.workspaceStorage, StorageScope.WORKSPACE, preserveData); } + + isProfileStorageFor(profile: IUserDataProfile): boolean { + return this.profileStorageProfile.id === profile.id; + } } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 84bca880ea4..a3d8c085cec 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -27,7 +27,8 @@ import { edit } from 'vs/platform/userDataSync/common/content'; 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 { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; const argvStoragePrefx = 'globalState.argv.'; const argvProperties: string[] = ['locale']; @@ -56,8 +57,6 @@ function stringify(globalState: IGlobalState, format: boolean): string { const GLOBAL_STATE_DATA_VERSION = 1; /** - * TODO: @sandy081: Sync only global state of default profile - * * Synchronises global state that includes * - Global storage with user scope * - Locale from argv properties @@ -77,6 +76,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); constructor( + private readonly profile: IUserDataProfile, + @IUserDataSyncProfilesStorageService private readonly userDataSyncProfilesStorageService: IUserDataSyncProfilesStorageService, @IFileService fileService: IFileService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @@ -85,7 +86,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, - @IStorageService private readonly storageService: IStorageService, + @IStorageService storageService: IStorageService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService); @@ -94,10 +95,17 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs Event.any( /* Locale change */ Event.filter(fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)), - /* Global storage with user target has changed */ - Event.filter(storageService.onDidChangeValue, e => e.scope === StorageScope.PROFILE && e.target !== undefined ? e.target === StorageTarget.USER : storageService.keys(StorageScope.PROFILE, StorageTarget.USER).includes(e.key)), - /* Storage key target has changed */ - this.storageService.onDidChangeTarget + Event.filter(this.userDataSyncProfilesStorageService.onDidChange, e => { + /* StorageTarget has changed in profile storage */ + if (e.targetChanges.some(profile => this.profile.id === profile.id)) { + return true; + } + /* User storage data has changed in profile storage */ + if (e.valueChanges.some(({ profile, changes }) => this.profile.id === profile.id && changes.some(change => change.target === StorageTarget.USER))) { + return true; + } + return false; + }), )((() => this.triggerLocalChange())) ); } @@ -117,7 +125,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`); } - const storageKeys = this.getStorageKeys(lastSyncGlobalState); + const storageKeys = await this.getStorageKeys(lastSyncGlobalState); const { local, remote } = merge(localGlobalState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, storageKeys, this.logService); const previewResult: IGlobalStateResourceMergeResult = { content: null, @@ -151,7 +159,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return true; } const localGlobalState = await this.getLocalGlobalState(); - const storageKeys = this.getStorageKeys(lastSyncGlobalState); + const storageKeys = await this.getStorageKeys(lastSyncGlobalState); const { remote } = merge(localGlobalState.storage, lastSyncGlobalState.storage, lastSyncGlobalState.storage, storageKeys, this.logService); return remote !== null; } @@ -303,10 +311,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] }; } } - for (const key of this.storageService.keys(StorageScope.PROFILE, StorageTarget.USER)) { - const value = this.storageService.get(key, StorageScope.PROFILE); - if (value) { - storage[key] = { version: 1, value }; + 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 }; } } return { storage }; @@ -326,7 +334,8 @@ 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: IStringDictionary<any> = {}; + const updatedStorage = new Map<string, string | undefined>(); + 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)) { @@ -335,12 +344,12 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } if (storage) { const storageValue = storage[key]; - if (storageValue.value !== String(this.storageService.get(key, StorageScope.PROFILE))) { - updatedStorage[key] = storageValue.value; + if (storageValue.value !== storageData.get(key)?.value) { + updatedStorage.set(key, storageValue.value); } } else { - if (this.storageService.get(key, StorageScope.PROFILE) !== undefined) { - updatedStorage[key] = undefined; + if (storageData.get(key) !== undefined) { + updatedStorage.set(key, undefined); } } } @@ -353,13 +362,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs await this.updateArgv(argv); this.logService.info(`${this.syncResourceLogLabel}: Updated locale`); } - const updatedStorageKeys: string[] = Object.keys(updatedStorage); - if (updatedStorageKeys.length) { + if (updatedStorage.size) { this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`); - for (const key of Object.keys(updatedStorage)) { - this.storageService.store(key, updatedStorage[key], StorageScope.PROFILE, StorageTarget.USER); - } - this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, Object.keys(updatedStorage)); + await this.userDataSyncProfilesStorageService.updateStorageData(this.profile, updatedStorage, StorageTarget.USER); + this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, [...updatedStorage.keys()]); } } @@ -376,11 +382,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } } - private getStorageKeys(lastSyncGlobalState: IGlobalState | null): StorageKeys { - const user = this.storageService.keys(StorageScope.PROFILE, StorageTarget.USER); - const machine = this.storageService.keys(StorageScope.PROFILE, StorageTarget.MACHINE); + private async getStorageKeys(lastSyncGlobalState: IGlobalState | null): Promise<StorageKeys> { + const storageData = await this.userDataSyncProfilesStorageService.readStorageData(this.profile); + const user: string[] = [], machine: string[] = []; + for (const [key, value] of storageData) { + if (value.target === StorageTarget.USER) { + user.push(key); + } else if (value.target === StorageTarget.MACHINE) { + machine.push(key); + } + } const registered = [...user, ...machine]; - const unregistered = lastSyncGlobalState?.storage ? Object.keys(lastSyncGlobalState.storage).filter(key => !key.startsWith(argvStoragePrefx) && !registered.includes(key) && this.storageService.get(key, StorageScope.PROFILE) !== undefined) : []; + const unregistered = lastSyncGlobalState?.storage ? Object.keys(lastSyncGlobalState.storage).filter(key => !key.startsWith(argvStoragePrefx) && !registered.includes(key) && storageData.get(key) !== undefined) : []; if (!isWeb) { // Following keys are synced only in web. Do not sync these keys in other platforms 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/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index d80bfe8fc23..a32f5e18752 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -824,7 +824,7 @@ class ProfileSynchronizer extends Disposable { case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser, this.profile.keybindingsResource); case SyncResource.Snippets: return this.instantiationService.createInstance(SnippetsSynchroniser, this.profile.snippetsHome); case SyncResource.Tasks: return this.instantiationService.createInstance(TasksSynchroniser, this.profile.tasksResource); - case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser); + case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser, this.profile); case SyncResource.Extensions: return this.instantiationService.createInstance(ExtensionsSynchroniser, this.profile.extensionsResource); } } 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 9c83435ab7f..ba4adbedc26 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -41,8 +41,10 @@ import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/pl import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { NullPolicyService } from 'vs/platform/policy/common/policy'; +import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { TestUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test'; export class UserDataSyncClient extends Disposable { @@ -88,7 +90,9 @@ export class UserDataSyncClient extends Disposable { const userDataProfilesService = this.instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); - this.instantiationService.stub(IStorageService, this._register(new InMemoryStorageService())); + const storageService = new TestStorageService(userDataProfilesService.defaultProfile); + this.instantiationService.stub(IStorageService, this._register(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(); @@ -302,3 +306,11 @@ export class TestUserDataSyncUtilService implements IUserDataSyncUtilService { } +class TestStorageService extends InMemoryStorageService { + constructor(private readonly profileStorageProfile: IUserDataProfile) { + super(); + } + 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'); + }); + +}); diff --git a/src/vs/workbench/services/storage/browser/storageService.ts b/src/vs/workbench/services/storage/browser/storageService.ts index 0266871f498..b92c3cfcc9b 100644 --- a/src/vs/workbench/services/storage/browser/storageService.ts +++ b/src/vs/workbench/services/storage/browser/storageService.ts @@ -56,21 +56,6 @@ export class BrowserStorageService extends AbstractStorageService { this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.switchToProfile(e.profile, e.preserveData)))); } - private getId(scope: StorageScope): string { - switch (scope) { - case StorageScope.APPLICATION: - return 'global'; // use the default profile application DB for application scope - case StorageScope.PROFILE: - if (isProfileUsingDefaultStorage(this.profileStorageProfile)) { - return 'global'; // default profile DB has a fixed name for backwards compatibility - } else { - return `global-${this.profileStorageProfile.id}`; - } - case StorageScope.WORKSPACE: - return this.payload.id; - } - } - protected async doInitialize(): Promise<void> { // Init storages @@ -82,7 +67,7 @@ export class BrowserStorageService extends AbstractStorageService { } private async createApplicationStorage(): Promise<void> { - const applicationStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.APPLICATION), broadcastChanges: true }, this.logService); + const applicationStorageIndexedDB = await IndexedDBStorageDatabase.createApplicationStorage(this.logService); this.applicationStorageDatabase = this._register(applicationStorageIndexedDB); this.applicationStorage = this._register(new Storage(this.applicationStorageDatabase)); @@ -118,7 +103,7 @@ export class BrowserStorageService extends AbstractStorageService { this.profileStorageDisposables.add(this.profileStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.PROFILE, key))); } else { - const profileStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.PROFILE), broadcastChanges: true }, this.logService); + const profileStorageIndexedDB = await IndexedDBStorageDatabase.createProfileStorage(this.profileStorageProfile, this.logService); this.profileStorageDatabase = this.profileStorageDisposables.add(profileStorageIndexedDB); this.profileStorage = this.profileStorageDisposables.add(new Storage(this.profileStorageDatabase)); @@ -132,7 +117,7 @@ export class BrowserStorageService extends AbstractStorageService { } private async createWorkspaceStorage(): Promise<void> { - const workspaceStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService); + const workspaceStorageIndexedDB = await IndexedDBStorageDatabase.createWorkspaceStorage(this.payload.id, this.logService); this.workspaceStorageDatabase = this._register(workspaceStorageIndexedDB); this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase)); @@ -165,7 +150,14 @@ export class BrowserStorageService extends AbstractStorageService { } protected getLogDetails(scope: StorageScope): string | undefined { - return this.getId(scope); + switch (scope) { + case StorageScope.APPLICATION: + return this.applicationStorageDatabase?.name; + case StorageScope.PROFILE: + return this.profileStorageDatabase?.name; + default: + return this.workspaceStorageDatabase?.name; + } } protected async switchToProfile(toProfile: IUserDataProfile, preserveData: boolean): Promise<void> { @@ -246,11 +238,20 @@ export class BrowserStorageService extends AbstractStorageService { this.workspaceStorageDatabase?.clear() ?? Promise.resolve() ]); } + + isProfileStorageFor(profile: IUserDataProfile): boolean { + return this.profileStorageProfile.id === profile.id; + } } interface IIndexedDBStorageDatabase extends IStorageDatabase, IDisposable { /** + * Name of the database. + */ + readonly name: string; + + /** * Whether an update in the DB is currently pending * (either update or delete operation). */ @@ -265,6 +266,7 @@ interface IIndexedDBStorageDatabase extends IStorageDatabase, IDisposable { class InMemoryIndexedDBStorageDatabase extends InMemoryStorageDatabase implements IIndexedDBStorageDatabase { readonly hasPendingUpdate = false; + readonly name = 'in-memory-indexedb-storage'; async clear(): Promise<void> { (await this.getItems()).clear(); @@ -282,6 +284,18 @@ interface IndexedDBStorageDatabaseOptions { export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBStorageDatabase { + static async createApplicationStorage(logService: ILogService): Promise<IIndexedDBStorageDatabase> { + return IndexedDBStorageDatabase.create({ id: 'global', broadcastChanges: true }, logService); + } + + static async createProfileStorage(profile: IUserDataProfile, logService: ILogService): Promise<IIndexedDBStorageDatabase> { + return IndexedDBStorageDatabase.create({ id: `global-${profile.id}`, broadcastChanges: true }, logService); + } + + static async createWorkspaceStorage(workspaceId: string, logService: ILogService): Promise<IIndexedDBStorageDatabase> { + return IndexedDBStorageDatabase.create({ id: workspaceId }, logService); + } + static async create(options: IndexedDBStorageDatabaseOptions, logService: ILogService): Promise<IIndexedDBStorageDatabase> { try { const database = new IndexedDBStorageDatabase(options, logService); @@ -298,8 +312,6 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt private static readonly STORAGE_DATABASE_PREFIX = 'vscode-web-state-db-'; private static readonly STORAGE_OBJECT_STORE = 'ItemTable'; - private static readonly STORAGE_BROADCAST_CHANNEL = 'vscode.web.state.changes'; - private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>()); readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; @@ -308,7 +320,7 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt private pendingUpdate: Promise<boolean> | undefined = undefined; get hasPendingUpdate(): boolean { return !!this.pendingUpdate; } - private readonly name: string; + readonly name: string; private readonly whenConnected: Promise<IndexedDB>; private constructor( @@ -318,7 +330,7 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt super(); this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${options.id}`; - this.broadcastChannel = options.broadcastChanges ? this._register(new BroadcastDataChannel<IStorageItemsChangeEvent>(IndexedDBStorageDatabase.STORAGE_BROADCAST_CHANNEL)) : undefined; + this.broadcastChannel = options.broadcastChanges ? this._register(new BroadcastDataChannel<IStorageItemsChangeEvent>(this.name)) : undefined; this.whenConnected = this.connect(); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts new file mode 100644 index 00000000000..7cec5ba8d7d --- /dev/null +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IStorageDatabase } from 'vs/base/parts/storage/common/storage'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractUserDataSyncProfilesStorageService, IProfileStorageChanges, IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { isProfileUsingDefaultStorage, IStorageService, IStorageValueChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IndexedDBStorageDatabase } from 'vs/workbench/services/storage/browser/storageService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +export class UserDataSyncProfilesStorageService extends AbstractUserDataSyncProfilesStorageService implements IUserDataSyncProfilesStorageService { + + private readonly _onDidChange = this._register(new Emitter<IProfileStorageChanges>()); + readonly onDidChange: Event<IProfileStorageChanges> = this._onDidChange.event; + + constructor( + @IStorageService storageService: IStorageService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @ILogService private readonly logService: ILogService, + ) { + super(storageService); + this._register(Event.filter(storageService.onDidChangeTarget, e => e.scope === StorageScope.PROFILE)(e => this.onDidChangeStorageTargetInCurrentProfile())); + this._register(Event.filter(storageService.onDidChangeValue, e => e.scope === StorageScope.PROFILE)(e => this.onDidChangeStorageValueInCurrentProfile(e))); + } + + private onDidChangeStorageTargetInCurrentProfile(): void { + // Not broadcasting changes to other windows/tabs as it is not required in web. + // Revisit if needed in future. + this._onDidChange.fire({ targetChanges: [this.userDataProfileService.currentProfile], valueChanges: [] }); + } + + private onDidChangeStorageValueInCurrentProfile(e: IStorageValueChangeEvent): void { + // Not broadcasting changes to other windows/tabs as it is not required in web + // Revisit if needed in future. + this._onDidChange.fire({ targetChanges: [], valueChanges: [{ profile: this.userDataProfileService.currentProfile, changes: [e] }] }); + } + + protected createStorageDatabase(profile: IUserDataProfile): Promise<IStorageDatabase> { + return isProfileUsingDefaultStorage(profile) ? IndexedDBStorageDatabase.createApplicationStorage(this.logService) : IndexedDBStorageDatabase.createProfileStorage(profile, this.logService); + } +} + +registerSingleton(IUserDataSyncProfilesStorageService, UserDataSyncProfilesStorageService, InstantiationType.Delayed); diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index f0fe083ef1b..d803e5293e8 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -63,6 +63,7 @@ import 'vs/workbench/services/files/browser/elevatedFileService'; import 'vs/workbench/services/workingCopy/browser/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import 'vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; |