Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/microsoft/vscode.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts3
-rw-r--r--src/vs/code/electron-main/app.ts5
-rw-r--r--src/vs/editor/contrib/find/test/browser/findController.test.ts6
-rw-r--r--src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts3
-rw-r--r--src/vs/platform/storage/common/storage.ts49
-rw-r--r--src/vs/platform/storage/electron-main/storageMainService.ts16
-rw-r--r--src/vs/platform/storage/electron-sandbox/storageService.ts4
-rw-r--r--src/vs/platform/userDataSync/common/globalStateSync.ts71
-rw-r--r--src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts151
-rw-r--r--src/vs/platform/userDataSync/common/userDataSyncService.ts2
-rw-r--r--src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts107
-rw-r--r--src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts52
-rw-r--r--src/vs/platform/userDataSync/test/common/userDataSyncClient.ts16
-rw-r--r--src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts119
-rw-r--r--src/vs/workbench/services/storage/browser/storageService.ts58
-rw-r--r--src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts48
-rw-r--r--src/vs/workbench/workbench.web.main.ts1
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';