src/vs/platform/userDataSync/common/abstractJsonSynchronizer.ts
276 LOC · 262 covered · 14 uncovered · 65 ranges · 585 concepts · 26 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 { CancellationToken } from '../../../base/common/cancellation.js';
import { URI } from '../../../base/common/uri.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { IFileService } from '../../files/common/files.js';
import { IStorageService } from '../../storage/common/storage.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IUserDataProfile } from '../../userDataProfile/common/userDataProfile.js';
import { AbstractFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from './abstractSynchronizer.js';
import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, USER_DATA_SYNC_SCHEME, SyncResource } from './userDataSync.js';
export interface IJsonResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
export abstract class AbstractJsonSynchronizer extends AbstractFileSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
private readonly previewResource: URI;
private readonly baseResource: URI;
private readonly localResource: URI;
private readonly remoteResource: URI;
private readonly acceptedResource: URI;
constructor(
syncResourceMetadata: { syncResource: SyncResource; profile: IUserDataProfile },
collection: string | undefined,
previewFileName: string,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLocalStoreService userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
) {
super(fileResource, syncResourceMetadata, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
this.previewResource = this.extUri.joinPath(this.syncPreviewFolder, previewFileName);
this.baseResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
this.localResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
this.remoteResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
this.acceptedResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
}
protected abstract getContentFromSyncContent(syncContent: string): string | null;
protected abstract toSyncContent(content: string | null): object;
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise<IJsonResourcePreview[]> {
const remoteContent = remoteUserData.syncData ? this.getContentFromSyncContent(remoteUserData.syncData.content) : null;
abstractJsonSynchronizer.ts ×8
// Use remote data as last sync data if last sync data does not exist and remote data is from same machine
lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData;
const lastSyncContent: string | null = lastSyncUserData?.syncData ? this.getContentFromSyncContent(lastSyncUserData.syncData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteUserData.syncData) {
const localContent = fileContent ? fileContent.value.toString() : null;
abstractJsonSynchronizer.ts ×3
if (!lastSyncContent // First time sync
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ${this.syncResource.syncResource} with local ${this.syncResource.syncResource}...`);
abstractJsonSynchronizer.ts ×1
const result = this.merge(localContent, remoteContent, lastSyncContent);
content = result.content;
hasConflicts = result.hasConflicts;
hasLocalChanged = result.hasLocalChanged;
hasRemoteChanged = result.hasRemoteChanged;
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote ${this.syncResource.syncResource} does not exist. Synchronizing ${this.syncResource.syncResource} for the first time.`);
abstractJsonSynchronizer.ts ×5
content = fileContent.value.toString();
hasRemoteChanged = true;
}
const previewResult: IMergeResult = {
content: hasConflicts ? lastSyncContent : content,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts
};
const localContent = fileContent ? fileContent.value.toString() : null;
return [{
fileContent,
baseResource: this.baseResource,
baseContent: lastSyncContent,
localResource: this.localResource,
localContent,
localChange: previewResult.localChange,
remoteResource: this.remoteResource,
remoteContent,
remoteChange: previewResult.remoteChange,
previewResource: this.previewResource,
previewResult,
acceptedResource: this.acceptedResource,
}];
}
protected async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise<boolean> {
const lastSyncContent: string | null = lastSyncUserData?.syncData ? this.getContentFromSyncContent(lastSyncUserData.syncData.content) : null;
abstractJsonSynchronizer.ts ×2
if (lastSyncContent === null) {
return true;
}
const fileContent = await this.getLocalFileContent();
const localContent = fileContent ? fileContent.value.toString() : null;
const result = this.merge(localContent, lastSyncContent, lastSyncContent);
return result.hasLocalChanged || result.hasRemoteChanged;
}
protected async getMergeResult(resourcePreview: IJsonResourcePreview, token: CancellationToken): Promise<IMergeResult> {
}
protected async getAcceptResult(resourcePreview: IJsonResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
if (this.extUri.isEqual(resource, this.localResource)) {
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
localChange: Change.None,
remoteChange: Change.Modified,
};
}
/* Accept remote resource */
if (this.extUri.isEqual(resource, this.remoteResource)) {
content: resourcePreview.remoteContent,
localChange: Change.Modified,
remoteChange: Change.None,
};
}
/* Accept preview resource */
if (this.extUri.isEqual(resource, this.previewResource)) {
if (content === undefined) {
return {
content: resourcePreview.previewResult.content,
localChange: resourcePreview.previewResult.localChange,
remoteChange: resourcePreview.previewResult.remoteChange,
};
} else {
content,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IJsonResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
const { content, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ${this.syncResource.syncResource}.`);
abstractJsonSynchronizer.ts ×1
}
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ${this.syncResource.syncResource}...`);
abstractJsonSynchronizer.ts ×4
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSyncContent(fileContent.value.toString())));
abstractJsonSynchronizer.ts ×1
}
await this.updateLocalFileContent(content, fileContent, force);
abstractJsonSynchronizer.ts ×3
}
this.logService.info(`${this.syncResourceLogLabel}: Updated local ${this.syncResource.syncResource}`);
abstractJsonSynchronizer.ts ×4
}
if (remoteChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ${this.syncResource.syncResource}...`);
abstractJsonSynchronizer.ts ×5
const remoteContents = JSON.stringify(this.toSyncContent(content));
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ${this.syncResource.syncResource}`);
abstractJsonSynchronizer.ts ×1
}
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ${this.syncResource.syncResource}...`);
abstractJsonSynchronizer.ts ×2
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ${this.syncResource.syncResource}`);
}
async hasLocalData(): Promise<boolean> {
return this.fileService.exists(this.file);
}
async resolveContent(uri: URI): Promise<string | null> {
|| this.extUri.isEqual(this.baseResource, uri)
|| this.extUri.isEqual(this.localResource, uri)
|| this.extUri.isEqual(this.acceptedResource, uri)
return this.resolvePreviewContent(uri);
}
return null;
private merge(originalLocalContent: string | null, originalRemoteContent: string | null, baseContent: string | null): {
hasLocalChanged: boolean;
hasRemoteChanged: boolean;
hasConflicts: boolean;
} {
/* no changes */
if (originalLocalContent === null && originalRemoteContent === null && baseContent === null) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
// Normalize nulls to empty strings for easier comparison
originalRemoteContent = originalRemoteContent ?? '';
originalLocalContent = originalLocalContent ?? '';
baseContent = baseContent ?? '';
/* no changes */
if (originalLocalContent === originalRemoteContent) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
abstractJsonSynchronizer.ts ×1
}
const localForwarded = baseContent !== originalLocalContent;
const remoteForwarded = baseContent !== originalRemoteContent;
/* no changes */
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
/* local has changed and remote has not */
return { content: originalLocalContent, hasRemoteChanged: true, hasLocalChanged: false, hasConflicts: false };
abstractJsonSynchronizer.ts ×1
}
/* remote has changed and local has not */
return { content: originalRemoteContent, hasLocalChanged: true, hasRemoteChanged: false, hasConflicts: false };
abstractJsonSynchronizer.ts ×3
}
return { content: originalLocalContent, hasLocalChanged: true, hasRemoteChanged: true, hasConflicts: true };