src/vs/platform/userDataSync/common/userDataSyncService.ts
948 LOC · 638 covered · 310 uncovered · 165 ranges · 585 concepts · 29 introducers · 348 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
abstractSynchronizer.ts ×49
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals } from '../../../base/common/arrays.js';
import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from '../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { toErrorMessage } from '../../../base/common/errorMessage.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { isEqual } from '../../../base/common/resources.js';
import { isBoolean, isUndefined } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IExtensionGalleryService } from '../../extensionManagement/common/extensionManagement.js';
import { IFileService } from '../../files/common/files.js';
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
import { ExtensionsSynchroniser } from './extensionsSync.js';
import { GlobalStateSynchroniser } from './globalStateSync.js';
import { KeybindingsSynchroniser } from './keybindingsSync.js';
import { PromptsSynchronizer } from './promptsSync/promptsSync.js';
import { SettingsSynchroniser } from './settingsSync.js';
import { SnippetsSynchroniser } from './snippetsSync.js';
import { TasksSynchroniser } from './tasksSync.js';
import { McpSynchroniser } from './mcpSync.js';
import { UserDataProfilesManifestSynchroniser } from './userDataProfilesManifestSync.js';
import {
ALL_SYNC_RESOURCES, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError,
IUserDataSyncResource, ISyncResourceHandle, IUserDataSyncTask, ISyncUserDataProfile, IUserDataManifest, IUserDataSyncConfiguration,
IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE, IUserDataSyncResourceProviderService, IUserDataSyncActivityData, IUserDataSyncLocalStoreService,
IUserDataSyncLatestData,
IUserData,
isUserDataManifest,
} from './userDataSync.js';
type SyncErrorClassification = {
owner: 'sandy081';
comment: 'Information about the error that occurred while syncing';
code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' };
service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service for which this error has occurred' };
serverCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service error code' };
url?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource URL for which this error has occurred' };
resource?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource for which this error has occurred' };
executionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync execution id for which this error has occurred' };
};
type SyncErrorEvent = {
code: string;
service: string;
serverCode?: string;
url?: string;
resource?: string;
executionId?: string;
};
const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
_serviceBrand: undefined;
private _status: SyncStatus = SyncStatus.Uninitialized;
get status(): SyncStatus { return this._status; }
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
private _onDidChangeLocal = this._register(new Emitter<SyncResource>());
readonly onDidChangeLocal = this._onDidChangeLocal.event;
private _conflicts: IUserDataSyncResourceConflicts[] = [];
get conflicts(): IUserDataSyncResourceConflicts[] { return this._conflicts; }
private _onDidChangeConflicts = this._register(new Emitter<IUserDataSyncResourceConflicts[]>());
readonly onDidChangeConflicts = this._onDidChangeConflicts.event;
private _syncErrors: IUserDataSyncResourceError[] = [];
private _onSyncErrors = this._register(new Emitter<IUserDataSyncResourceError[]>());
readonly onSyncErrors = this._onSyncErrors.event;
private _lastSyncTime: number | undefined = undefined;
get lastSyncTime(): number | undefined { return this._lastSyncTime; }
private _onDidChangeLastSyncTime: Emitter<number> = this._register(new Emitter<number>());
readonly onDidChangeLastSyncTime: Event<number> = this._onDidChangeLastSyncTime.event;
private _onDidResetLocal = this._register(new Emitter<void>());
readonly onDidResetLocal = this._onDidResetLocal.event;
private _onDidResetRemote = this._register(new Emitter<void>());
readonly onDidResetRemote = this._onDidResetRemote.event;
private activeProfileSynchronizers = new Map<string, [ProfileSynchronizer, IDisposable]>();
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
@IUserDataSyncResourceProviderService private readonly userDataSyncResourceProviderService: IUserDataSyncResourceProviderService,
@IUserDataSyncLocalStoreService private readonly userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
) {
super();
this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized;
this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION, undefined);
this._register(toDisposable(() => this.clearActiveProfileSynchronizers()));
this._register(new RunOnceScheduler(() => this.cleanUpStaleStorageData(), 5 * 1000 /* after 5s */)).schedule();
}
async createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise<IUserDataSyncTask> {
this.logService.info('Sync started.');
const startTime = new Date().getTime();
const executionId = generateUuid();
try {
const syncHeaders = createSyncHeaders(executionId);
if (disableCache) {
}
manifest = await this.userDataSyncStoreService.manifest(manifest, syncHeaders);
userDataSyncService.ts ×22
} catch (error) {
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
throw userDataSyncError;
}
const executed = false;
const that = this;
let cancellablePromise: CancelablePromise<void> | undefined;
return {
manifest,
async run(): Promise<void> {
if (executed) {
throw new Error('Can run a task only once');
}
cancellablePromise = createCancelablePromise(token => that.sync(manifest, false, executionId, token));
userDataSyncService.ts ×22
await cancellablePromise.finally(() => cancellablePromise = undefined);
that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
extensionsSync.ts ×17
that.updateLastSyncTime();
stop(): Promise<void> {
return that.stop();
}
}
async createManualSyncTask(): Promise<IUserDataManualSyncTask> {
this.checkEnablement();
if (this.userDataSyncEnablementService.isEnabled()) {
throw new UserDataSyncError('Cannot start manual sync when sync is enabled', UserDataSyncErrorCode.LocalError);
}
this.logService.info('Sync started.');
const startTime = new Date().getTime();
const executionId = generateUuid();
const syncHeaders = createSyncHeaders(executionId);
let latestUserDataOrManifest: IUserDataSyncLatestData | IUserDataManifest | null;
try {
latestUserDataOrManifest = await this.userDataSyncStoreService.getLatestData(syncHeaders);
} catch (error) {
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<SyncErrorEvent, SyncErrorClassification>('sync.download.latest',
{
code: userDataSyncError.code,
serverCode: userDataSyncError instanceof UserDataSyncStoreError ? String(userDataSyncError.serverCode) : undefined,
url: userDataSyncError instanceof UserDataSyncStoreError ? userDataSyncError.url : undefined,
resource: userDataSyncError.resource,
executionId,
service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString()
});
// Fallback to manifest in stable
try {
latestUserDataOrManifest = await this.userDataSyncStoreService.manifest(null, syncHeaders);
} catch (error) {
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
throw userDataSyncError;
}
}
/* Manual sync shall start on clean local state */
await this.resetLocal();
const that = this;
const cancellableToken = new CancellationTokenSource();
return {
id: executionId,
async merge(): Promise<void> {
return that.sync(latestUserDataOrManifest, true, executionId, cancellableToken.token);
},
async apply(): Promise<void> {
try {
try {
await that.applyManualSync(latestUserDataOrManifest, executionId, cancellableToken.token);
} catch (error) {
if (UserDataSyncError.toUserDataSyncError(error).code === UserDataSyncErrorCode.MethodNotFound) {
that.logService.info('Client is making invalid requests. Cleaning up data...');
await that.cleanUpRemoteData();
that.logService.info('Applying manual sync again...');
await that.applyManualSync(latestUserDataOrManifest, executionId, cancellableToken.token);
} else {
throw error;
}
}
} catch (error) {
that.logService.error(error);
throw error;
}
that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
that.updateLastSyncTime();
},
async stop(): Promise<void> {
cancellableToken.cancel();
await that.stop();
await that.resetLocal();
}
};
}
private async sync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<void> {
try {
if (this.status !== SyncStatus.HasConflicts) {
this.setStatus(SyncStatus.Syncing);
}
// Sync Default Profile First
const defaultProfileSynchronizer = this.getOrCreateActiveProfileSynchronizer(this.userDataProfilesService.defaultProfile, undefined);
this._syncErrors.push(...await this.syncProfile(defaultProfileSynchronizer, manifestOrLatestData, preview, executionId, token));
// Sync other profiles
const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles);
if (userDataProfileManifestSynchronizer) {
const syncProfiles = (await (userDataProfileManifestSynchronizer as UserDataProfilesManifestSynchroniser).getLastSyncedProfiles()) || [];
if (token.isCancellationRequested) {
}
await this.syncRemoteProfiles(syncProfiles, manifestOrLatestData, preview, executionId, token);
extensionsSync.ts ×17
}
if (this.status !== SyncStatus.HasConflicts) {
this.setStatus(SyncStatus.Idle);
}
this._onSyncErrors.fire(this._syncErrors);
}
}
private async syncRemoteProfiles(remoteProfiles: ISyncUserDataProfile[], manifest: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<void> {
return;
}
const profile = this.userDataProfilesService.profiles.find(p => p.id === syncProfile.id);
userDataSyncService.ts ×4
if (!profile) {
this.logService.error(`Profile with id:${syncProfile.id} and name: ${syncProfile.name} does not exist locally to sync.`);
continue;
}
const profileSynchronizer = this.getOrCreateActiveProfileSynchronizer(profile, syncProfile);
this._syncErrors.push(...await this.syncProfile(profileSynchronizer, manifest, preview, executionId, token));
}
for (const [key, profileSynchronizerItem] of this.activeProfileSynchronizers.entries()) {
if (this.userDataProfilesService.profiles.some(p => p.id === profileSynchronizerItem[0].profile.id)) {
continue;
}
profileSynchronizerItem[1].dispose();
this.activeProfileSynchronizers.delete(key);
}
private async applyManualSync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, executionId: string, token: CancellationToken): Promise<void> {
try {
this.setStatus(SyncStatus.Syncing);
const profileSynchronizers = this.getActiveProfileSynchronizers();
for (const profileSynchronizer of profileSynchronizers) {
if (token.isCancellationRequested) {
return;
}
await profileSynchronizer.apply(executionId, token);
}
const defaultProfileSynchronizer = profileSynchronizers.find(s => s.profile.isDefault);
if (!defaultProfileSynchronizer) {
return;
}
const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles);
if (!userDataProfileManifestSynchronizer) {
return;
}
// Sync remote profiles which are not synced locally
const remoteProfiles = (await (userDataProfileManifestSynchronizer as UserDataProfilesManifestSynchroniser).getRemoteSyncedProfiles(getRefOrUserData(manifestOrLatestData, undefined, SyncResource.Profiles) ?? null)) || [];
const remoteProfilesToSync = remoteProfiles.filter(remoteProfile => profileSynchronizers.every(s => s.profile.id !== remoteProfile.id));
if (remoteProfilesToSync.length) {
await this.syncRemoteProfiles(remoteProfilesToSync, manifestOrLatestData, false, executionId, token);
}
} finally {
this.setStatus(SyncStatus.Idle);
}
}
private async syncProfile(profileSynchronizer: ProfileSynchronizer, manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<IUserDataSyncResourceError[]> {
const errors = await profileSynchronizer.sync(manifestOrLatestData, preview, executionId, token);
userDataSyncService.ts ×22
return errors.map(([syncResource, error]) => ({ profile: profileSynchronizer.profile, syncResource, error }));
extensionsSync.ts ×17
private async stop(): Promise<void> {
await Promise.allSettled(this.getActiveProfileSynchronizers().map(profileSynchronizer => profileSynchronizer.stop()));
}
}
async resolveContent(resource: URI): Promise<string | null> {
const content = await this.userDataSyncResourceProviderService.resolveContent(resource);
if (content) {
return content;
}
for (const profileSynchronizer of this.getActiveProfileSynchronizers()) {
for (const synchronizer of profileSynchronizer.enabled) {
const content = await synchronizer.resolveContent(resource);
if (content) {
return content;
}
}
}
return null;
}
async replace(syncResourceHandle: ISyncResourceHandle): Promise<void> {
this.checkEnablement();
const profileSyncResource = this.userDataSyncResourceProviderService.resolveUserDataSyncResource(syncResourceHandle);
if (!profileSyncResource) {
return;
}
const content = await this.resolveContent(syncResourceHandle.uri);
if (!content) {
return;
}
await this.performAction(profileSyncResource.profile, async synchronizer => {
if (profileSyncResource.syncResource === synchronizer.resource) {
await synchronizer.replace(content);
return true;
}
return undefined;
});
return;
}
async accept(syncResource: IUserDataSyncResource, resource: URI, content: string | null | undefined, apply: boolean | { force: boolean }): Promise<void> {
this.checkEnablement();
await this.performAction(syncResource.profile, async synchronizer => {
if (syncResource.syncResource === synchronizer.resource) {
await synchronizer.accept(resource, content);
if (apply) {
await synchronizer.apply(isBoolean(apply) ? false : apply.force, createSyncHeaders(generateUuid()));
}
return true;
}
return undefined;
});
}
async hasLocalData(): Promise<boolean> {
const result = await this.performAction(this.userDataProfilesService.defaultProfile, async synchronizer => {
// skip global state synchronizer
if (synchronizer.resource !== SyncResource.GlobalState && await synchronizer.hasLocalData()) {
return true;
}
return undefined;
});
return !!result;
}
async hasPreviouslySynced(): Promise<boolean> {
const result = await this.performAction(this.userDataProfilesService.defaultProfile, async synchronizer => {
userDataSyncService.ts ×9
if (await synchronizer.hasPreviouslySynced()) {
}
return !!result;
}
async reset(): Promise<void> {
await this.resetRemote();
await this.resetLocal();
}
async resetRemote(): Promise<void> {
try {
await this.userDataSyncStoreService.clear();
this.logService.info('Cleared data on server');
} catch (e) {
this.logService.error(e);
}
}
async resetLocal(): Promise<void> {
this._lastSyncTime = undefined;
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION);
for (const [synchronizer] of this.activeProfileSynchronizers.values()) {
try {
await synchronizer.resetLocal();
} catch (e) {
this.logService.error(e);
}
this.clearActiveProfileSynchronizers();
this._onDidResetLocal.fire();
this.logService.info('Did reset the local sync state.');
}
private async cleanUpStaleStorageData(): Promise<void> {
const allKeys = this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE);
userDataSyncService.ts ×5
const lastSyncProfileKeys: [string, string][] = [];
for (const key of allKeys) {
if (!key.endsWith('.lastSyncUserData')) {
continue;
}
if (segments.length === 3) {
}
if (!lastSyncProfileKeys.length) {
}
const disposables = new DisposableStore();
try {
let defaultProfileSynchronizer = this.activeProfileSynchronizers.get(this.userDataProfilesService.defaultProfile.id)?.[0];
defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, this.userDataProfilesService.defaultProfile, undefined));
}
const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles) as UserDataProfilesManifestSynchroniser;
userDataSyncService.ts ×6
if (!userDataProfileManifestSynchronizer) {
return;
}
const lastSyncedProfiles = await userDataProfileManifestSynchronizer.getLastSyncedProfiles();
userDataSyncService.ts ×6
const lastSyncedCollections = lastSyncedProfiles?.map(p => p.collection) ?? [];
userDataSyncService.ts ×5
for (const [key, collection] of lastSyncProfileKeys) {
this.logService.info(`Removing last sync state for stale profile: ${collection}`);
this.storageService.remove(key, StorageScope.APPLICATION);
}
} finally {
disposables.dispose();
}
async cleanUpRemoteData(): Promise<void> {
const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles();
const remoteProfileCollections = remoteProfiles.map(profile => profile.collection);
const allCollections = await this.userDataSyncStoreService.getAllCollections();
const redundantCollections = allCollections.filter(c => !remoteProfileCollections.includes(c));
if (redundantCollections.length) {
this.logService.info(`Deleting ${redundantCollections.length} redundant collections on server`);
await Promise.allSettled(redundantCollections.map(collectionId => this.userDataSyncStoreService.deleteCollection(collectionId)));
this.logService.info(`Deleted redundant collections on server`);
}
const updatedRemoteProfiles = remoteProfiles.filter(profile => allCollections.includes(profile.collection));
if (updatedRemoteProfiles.length !== remoteProfiles.length) {
const profileManifestSynchronizer = this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.userDataProfilesService.defaultProfile, undefined);
try {
this.logService.info('Resetting the last synced state of profiles');
await profileManifestSynchronizer.resetLocal();
this.logService.info('Did reset the last synced state of profiles');
this.logService.info(`Updating remote profiles with invalid collections on server`);
await profileManifestSynchronizer.updateRemoteProfiles(updatedRemoteProfiles, null);
this.logService.info(`Updated remote profiles on server`);
} finally {
profileManifestSynchronizer.dispose();
}
}
}
async saveRemoteActivityData(location: URI): Promise<void> {
this.checkEnablement();
const data = await this.userDataSyncStoreService.getActivityData();
await this.fileService.writeFile(location, data);
}
async extractActivityData(activityDataResource: URI, location: URI): Promise<void> {
const content = (await this.fileService.readFile(activityDataResource)).value.toString();
const activityData: IUserDataSyncActivityData = JSON.parse(content);
if (activityData.resources) {
for (const resource in activityData.resources) {
for (const version of activityData.resources[resource]) {
await this.userDataSyncLocalStoreService.writeResource(resource as SyncResource, version.content, new Date(version.created * 1000), undefined, location);
}
}
}
if (activityData.collections) {
for (const collection in activityData.collections) {
for (const resource in activityData.collections[collection].resources) {
for (const version of activityData.collections[collection].resources?.[resource] ?? []) {
await this.userDataSyncLocalStoreService.writeResource(resource as SyncResource, version.content, new Date(version.created * 1000), collection, location);
}
}
}
}
}
private async performAction<T>(profile: IUserDataProfile, action: (synchroniser: IUserDataSynchroniser) => Promise<T | undefined>): Promise<T | null> {
try {
const activeProfileSyncronizer = this.activeProfileSynchronizers.get(profile.id);
if (activeProfileSyncronizer) {
const result = await this.performActionWithProfileSynchronizer(activeProfileSyncronizer[0], action, disposables);
userDataSyncService.ts ×3
return isUndefined(result) ? null : result;
}
if (profile.isDefault) {
const defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, undefined));
const result = await this.performActionWithProfileSynchronizer(defaultProfileSynchronizer, action, disposables);
return isUndefined(result) ? null : result;
}
const userDataProfileManifestSynchronizer = disposables.add(this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, profile, undefined));
const manifest = await this.userDataSyncStoreService.manifest(null);
const syncProfiles = (await userDataProfileManifestSynchronizer.getRemoteSyncedProfiles(manifest?.latest?.profiles ?? null)) || [];
userDataSyncService.ts ×9
const syncProfile = syncProfiles.find(syncProfile => syncProfile.id === profile.id);
if (syncProfile) {
const profileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, syncProfile.collection));
const result = await this.performActionWithProfileSynchronizer(profileSynchronizer, action, disposables);
return isUndefined(result) ? null : result;
}
return null;
disposables.dispose();
}
}
private async performActionWithProfileSynchronizer<T>(profileSynchronizer: ProfileSynchronizer, action: (synchroniser: IUserDataSynchroniser) => Promise<T | undefined>, disposables: DisposableStore): Promise<T | undefined> {
const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.reduce<(IUserDataSynchroniser & IDisposable)[]>((synchronizers, syncResource) => {
userDataSyncService.ts ×9
if (syncResource !== SyncResource.WorkspaceState) {
synchronizers.push(disposables.add(profileSynchronizer.createSynchronizer(syncResource)));
}
return synchronizers;
for (const synchronizer of allSynchronizers) {
const result = await action(synchronizer);
if (!isUndefined(result)) {
}
private setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangeStatus.fire(status);
if (oldStatus === SyncStatus.HasConflicts) {
}
}
private updateConflicts(): void {
const conflicts = this.getActiveProfileSynchronizers().map(synchronizer => synchronizer.conflicts).flat();
userDataSyncService.ts ×4
if (!equals(this._conflicts, conflicts, (a, b) => a.profile.id === b.profile.id && a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(conflicts);
}
}
private updateLastSyncTime(): void {
this._lastSyncTime = new Date().getTime();
this.storageService.store(LAST_SYNC_TIME_KEY, this._lastSyncTime, StorageScope.APPLICATION, StorageTarget.MACHINE);
this._onDidChangeLastSyncTime.fire(this._lastSyncTime);
}
}
getOrCreateActiveProfileSynchronizer(profile: IUserDataProfile, syncProfile: ISyncUserDataProfile | undefined): ProfileSynchronizer {
let activeProfileSynchronizer = this.activeProfileSynchronizers.get(profile.id);
userDataSyncService.ts ×19
if (activeProfileSynchronizer && activeProfileSynchronizer[0].collection !== syncProfile?.collection) {
this.logService.error('Profile synchronizer collection does not match with the remote sync profile collection');
activeProfileSynchronizer[1].dispose();
activeProfileSynchronizer = undefined;
this.activeProfileSynchronizers.delete(profile.id);
}
const disposables = new DisposableStore();
const profileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, syncProfile?.collection));
disposables.add(profileSynchronizer.onDidChangeStatus(e => this.setStatus(e)));
disposables.add(profileSynchronizer.onDidChangeConflicts(conflicts => this.updateConflicts()));
disposables.add(profileSynchronizer.onDidChangeLocal(e => this._onDidChangeLocal.fire(e)));
this.activeProfileSynchronizers.set(profile.id, activeProfileSynchronizer = [profileSynchronizer, disposables]);
}
return activeProfileSynchronizer[0];
}
private getActiveProfileSynchronizers(): ProfileSynchronizer[] {
for (const [profileSynchronizer] of this.activeProfileSynchronizers.values()) {
profileSynchronizers.push(profileSynchronizer);
}
return profileSynchronizers;
}
private clearActiveProfileSynchronizers(): void {
this.activeProfileSynchronizers.forEach(([, disposable]) => disposable.dispose());
userDataSyncStoreService.ts ×36
this.activeProfileSynchronizers.clear();
}
private checkEnablement(): void {
throw new Error('Not enabled');
}
}
class ProfileSynchronizer extends Disposable {
private _enabled: [IUserDataSynchroniser, number, IDisposable][] = [];
get enabled(): IUserDataSynchroniser[] { return this._enabled.sort((a, b) => a[1] - b[1]).map(([synchronizer]) => synchronizer); }
get disabled(): SyncResource[] { return ALL_SYNC_RESOURCES.filter(syncResource => !this.userDataSyncEnablementService.isResourceEnabled(syncResource)); }
private _status: SyncStatus = SyncStatus.Idle;
get status(): SyncStatus { return this._status; }
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
private _onDidChangeLocal = this._register(new Emitter<SyncResource>());
readonly onDidChangeLocal = this._onDidChangeLocal.event;
private _conflicts: IUserDataSyncResourceConflicts[] = [];
get conflicts(): IUserDataSyncResourceConflicts[] { return this._conflicts; }
private _onDidChangeConflicts = this._register(new Emitter<IUserDataSyncResourceConflicts[]>());
readonly onDidChangeConflicts = this._onDidChangeConflicts.event;
constructor(
readonly collection: string | undefined,
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this._register(userDataSyncEnablementService.onDidChangeResourceEnablement(([syncResource, enablement]) => this.onDidChangeResourceEnablement(syncResource, enablement)));
this._register(toDisposable(() => this._enabled.splice(0, this._enabled.length).forEach(([, , disposable]) => disposable.dispose())));
for (const syncResource of ALL_SYNC_RESOURCES) {
if (userDataSyncEnablementService.isResourceEnabled(syncResource)) {
this.registerSynchronizer(syncResource);
}
}
}
private onDidChangeResourceEnablement(syncResource: SyncResource, enabled: boolean): void {
this.registerSynchronizer(syncResource);
this.deRegisterSynchronizer(syncResource);
}
}
protected registerSynchronizer(syncResource: SyncResource): void {
if (this._enabled.some(([synchronizer]) => synchronizer.resource === syncResource)) {
userDataSyncService.ts ×19
return;
}
if (syncResource === SyncResource.Extensions && !this.extensionGalleryService.isEnabled()) {
userDataSyncService.ts ×19
this.logService.info('Skipping extensions sync because gallery is not configured');
return;
}
if (!this.profile.isDefault) {
}
if (syncResource === SyncResource.WorkspaceState) {
return;
}
if (syncResource !== SyncResource.Profiles && this.profile.useDefaultFlags?.[syncResource]) {
userDataSyncService.ts ×19
this.logService.debug(`Skipping syncing ${syncResource} in ${this.profile.name} because it is already synced by default profile`);
userDataSyncService.ts ×1
return;
}
const synchronizer = disposables.add(this.createSynchronizer(syncResource));
disposables.add(synchronizer.onDidChangeStatus(() => this.updateStatus()));
disposables.add(synchronizer.onDidChangeConflicts(() => this.updateConflicts()));
disposables.add(synchronizer.onDidChangeLocal(() => this._onDidChangeLocal.fire(syncResource)));
const order = this.getOrder(syncResource);
this._enabled.push([synchronizer, order, disposables]);
}
private deRegisterSynchronizer(syncResource: SyncResource): void {
const index = this._enabled.findIndex(([synchronizer]) => synchronizer.resource === syncResource);
userDataSyncService.ts ×3
if (index !== -1) {
const [[synchronizer, , disposable]] = this._enabled.splice(index, 1);
disposable.dispose();
this.updateStatus();
synchronizer.stop().then(null, error => this.logService.error(error));
}
}
createSynchronizer(syncResource: Exclude<SyncResource, SyncResource.WorkspaceState>): IUserDataSynchroniser & IDisposable {
case SyncResource.Settings: return this.instantiationService.createInstance(SettingsSynchroniser, this.profile, this.collection);
case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser, this.profile, this.collection);
case SyncResource.Snippets: return this.instantiationService.createInstance(SnippetsSynchroniser, this.profile, this.collection);
case SyncResource.Prompts: return this.instantiationService.createInstance(PromptsSynchronizer, this.profile, this.collection);
case SyncResource.Tasks: return this.instantiationService.createInstance(TasksSynchroniser, this.profile, this.collection);
case SyncResource.Mcp: return this.instantiationService.createInstance(McpSynchroniser, this.profile, this.collection);
case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser, this.profile, this.collection);
case SyncResource.Extensions: return this.instantiationService.createInstance(ExtensionsSynchroniser, this.profile, this.collection);
case SyncResource.Profiles: return this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.profile, this.collection);
}
}
async sync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<[SyncResource, UserDataSyncError][]> {
// Return if cancellation is requested
if (token.isCancellationRequested) {
return [];
}
const synchronizers = this.enabled;
if (!synchronizers.length) {
return [];
}
try {
const syncErrors: [SyncResource, UserDataSyncError][] = [];
const syncHeaders = createSyncHeaders(executionId);
const userDataSyncConfiguration = preview ? await this.getUserDataSyncConfiguration(manifestOrLatestData) : this.getLocalUserDataSyncConfiguration();
for (const synchroniser of synchronizers) {
// Return if cancellation is requested
if (token.isCancellationRequested) {
}
// Return if resource is not enabled
if (!this.userDataSyncEnablementService.isResourceEnabled(synchroniser.resource)) {
return [];
}
try {
const refOrUserData = getRefOrUserData(manifestOrLatestData, this.collection, synchroniser.resource) ?? null;
await synchroniser.sync(refOrUserData, preview, userDataSyncConfiguration, syncHeaders);
} catch (e) {
reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
if (canBailout(e)) {
throw userDataSyncError;
}
// Log and and continue
this.logService.error(e);
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
syncErrors.push([synchroniser.resource, userDataSyncError]);
}
return syncErrors;
this.updateStatus();
}
}
async apply(executionId: string, token: CancellationToken): Promise<void> {
const syncHeaders = createSyncHeaders(executionId);
for (const synchroniser of this.enabled) {
if (token.isCancellationRequested) {
return;
}
try {
await synchroniser.apply(false, syncHeaders);
} catch (e) {
const userDataSyncError = UserDataSyncError.toUserDataSyncError(e);
reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
if (canBailout(e)) {
throw userDataSyncError;
}
// Log and and continue
this.logService.error(e);
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
}
}
}
async stop(): Promise<void> {
try {
if (synchroniser.status !== SyncStatus.Idle) {
await synchroniser.stop();
}
} catch (e) {
this.logService.error(e);
}
}
async resetLocal(): Promise<void> {
try {
await synchroniser.resetLocal();
} catch (e) {
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
this.logService.error(e);
}
}
private async getUserDataSyncConfiguration(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null): Promise<IUserDataSyncConfiguration> {
if (!this.profile.isDefault) {
return {};
}
const local = this.getLocalUserDataSyncConfiguration();
const settingsSynchronizer = this.enabled.find(synchronizer => synchronizer instanceof SettingsSynchroniser);
if (settingsSynchronizer) {
const remote = await settingsSynchronizer.getRemoteUserDataSyncConfiguration(getRefOrUserData(manifestOrLatestData, this.collection, SyncResource.Settings) ?? null);
return { ...local, ...remote };
}
return local;
}
private getLocalUserDataSyncConfiguration(): IUserDataSyncConfiguration {
return this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE);
userDataSyncService.ts ×22
}
private setStatus(status: SyncStatus): void {
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
private updateStatus(): void {
if (this.enabled.some(s => s.status === SyncStatus.HasConflicts)) {
}
return this.setStatus(SyncStatus.Syncing);
}
private updateConflicts(): void {
const conflicts = this.enabled.filter(s => s.status === SyncStatus.HasConflicts)
userDataSyncService.ts ×19
.filter(s => s.conflicts.conflicts.length > 0)
.map(s => s.conflicts);
if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
this._onDidChangeConflicts.fire(conflicts);
}
private getOrder(syncResource: SyncResource): number {
case SyncResource.Settings: return 0;
case SyncResource.Keybindings: return 1;
case SyncResource.Snippets: return 2;
case SyncResource.Tasks: return 3;
case SyncResource.Mcp: return 4;
case SyncResource.GlobalState: return 5;
case SyncResource.Extensions: return 6;
case SyncResource.Prompts: return 7;
case SyncResource.Profiles: return 8;
case SyncResource.WorkspaceState: return 9;
}
}
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.MethodNotFound:
case UserDataSyncErrorCode.TooLarge:
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter:
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.LocalTooManyProfiles:
case UserDataSyncErrorCode.Gone:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.IncompatibleRemoteContent:
case UserDataSyncErrorCode.IncompatibleLocalContent:
return true;
}
}
return false;
}
function reportUserDataSyncError(userDataSyncError: UserDataSyncError, executionId: string, userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, telemetryService: ITelemetryService): void {
userDataSyncService.ts ×3
telemetryService.publicLog2<SyncErrorEvent, SyncErrorClassification>('sync/error',
{
code: userDataSyncError.code,
serverCode: userDataSyncError instanceof UserDataSyncStoreError ? String(userDataSyncError.serverCode) : undefined,
url: userDataSyncError instanceof UserDataSyncStoreError ? userDataSyncError.url : undefined,
resource: userDataSyncError.resource,
executionId,
service: userDataSyncStoreManagementService.userDataSyncStore!.url.toString()
});
}
function getRefOrUserData(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, collection: string | undefined, resource: SyncResource): string | IUserData | undefined {
userDataSyncService.ts ×22
if (isUserDataManifest(manifestOrLatestData)) {
return manifestOrLatestData?.collections?.[collection]?.latest?.[resource];
userDataSyncService.ts ×1
}
}
return manifestOrLatestData?.collections?.[collection]?.resources?.[resource];
userDataSyncService.ts ×1
}