Merge from vscode 1b314ab317fbff7d799b21754326b7d849889ceb

This commit is contained in:
ADS Merger
2020-07-15 23:51:18 +00:00
parent aae013d498
commit 9d3f12d0b7
554 changed files with 15159 additions and 8223 deletions

View File

@@ -9,7 +9,8 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import {
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
@@ -52,9 +53,20 @@ function isSyncData(thing: any): thing is ISyncData {
return false;
}
export interface IMergableResourcePreview extends IBaseResourcePreview {
readonly remoteContent: string | null;
readonly localContent: string | null;
readonly previewContent: string | null;
readonly hasConflicts: boolean;
merged: boolean;
}
export type IResourcePreview = Omit<IMergableResourcePreview, 'merged'>;
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: IRemoteUserData | null;
readonly resourcePreviews: IMergableResourcePreview[];
}
export abstract class AbstractSynchroniser extends Disposable {
@@ -70,10 +82,10 @@ export abstract class AbstractSynchroniser extends Disposable {
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private _conflicts: Conflict[] = [];
get conflicts(): Conflict[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<Conflict[]> = this._register(new Emitter<Conflict[]>());
readonly onDidChangeConflicts: Event<Conflict[]> = this._onDidChangeConflicts.event;
private _conflicts: IMergableResourcePreview[] = [];
get conflicts(): IMergableResourcePreview[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<IMergableResourcePreview[]> = this._register(new Emitter<IMergableResourcePreview[]>());
readonly onDidChangeConflicts: Event<IMergableResourcePreview[]> = this._onDidChangeConflicts.event;
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
@@ -119,7 +131,7 @@ export abstract class AbstractSynchroniser extends Disposable {
this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`);
const preview = await this.syncPreviewPromise!;
this.syncPreviewPromise = null;
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData);
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true);
this.setStatus(status);
}
@@ -127,7 +139,7 @@ export abstract class AbstractSynchroniser extends Disposable {
else {
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
const lastSyncUserData = await this.getLastSyncUserData();
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true;
const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true;
if (hasRemoteChanged) {
this._onDidChangeLocal.fire();
}
@@ -137,8 +149,6 @@ export abstract class AbstractSynchroniser extends Disposable {
protected setStatus(status: SyncStatus): void {
if (this._status !== status) {
const oldStatus = this._status;
this._status = status;
this._onDidChangStatus.fire(status);
if (status === SyncStatus.HasConflicts) {
// Log to telemetry when there is a sync conflict
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.resource });
@@ -147,16 +157,8 @@ export abstract class AbstractSynchroniser extends Disposable {
// Log to telemetry when conflicts are resolved
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource });
}
if (this.status !== SyncStatus.HasConflicts) {
this.setConflicts([]);
}
}
}
protected setConflicts(conflicts: Conflict[]) {
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(this._conflicts);
this._status = status;
this._onDidChangStatus.fire(status);
}
}
@@ -176,7 +178,7 @@ export abstract class AbstractSynchroniser extends Disposable {
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
await this.applyPreview(preview, false);
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
@@ -199,7 +201,7 @@ export abstract class AbstractSynchroniser extends Disposable {
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
await this.applyPreview(preview, true);
await this.applyPreview(remoteUserData, lastSyncUserData, preview, true);
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
} finally {
@@ -208,38 +210,49 @@ export abstract class AbstractSynchroniser extends Disposable {
}
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
await this._sync(manifest, true, headers);
}
async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
return this._sync(manifest, false, headers);
}
private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
try {
this.syncHeaders = { ...headers };
if (!this.isEnabled()) {
if (this.status !== SyncStatus.Idle) {
await this.stop();
}
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
return;
return null;
}
if (this.status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
return;
return this.syncPreviewPromise;
}
if (this.status === SyncStatus.Syncing) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
return;
return this.syncPreviewPromise;
}
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
let status: SyncStatus = SyncStatus.Idle;
try {
status = await this.performSync(remoteUserData, lastSyncUserData);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
status = await this.performSync(remoteUserData, lastSyncUserData, apply);
if (status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
} else if (status === SyncStatus.Idle) {
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
}
return this.syncPreviewPromise || null;
} finally {
this.setStatus(status);
}
@@ -267,7 +280,7 @@ export abstract class AbstractSynchroniser extends Disposable {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
await this.applyPreview(preview, false);
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
@@ -276,7 +289,7 @@ export abstract class AbstractSynchroniser extends Disposable {
return true;
}
private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (lastSyncUserData) {
const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
@@ -294,24 +307,15 @@ export abstract class AbstractSynchroniser extends Disposable {
return this.getRemoteUserData(lastSyncUserData);
}
async generateSyncPreview(): Promise<ISyncResourcePreview | null> {
if (this.isEnabled()) {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
}
return null;
}
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
// current version is not compatible with cloud version
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.IncompatibleLocalContent, this.resource);
}
try {
return await this.doSync(remoteUserData, lastSyncUserData);
return await this.doSync(remoteUserData, lastSyncUserData, apply);
} catch (e) {
if (e instanceof UserDataSyncError) {
switch (e.code) {
@@ -319,7 +323,7 @@ export abstract class AbstractSynchroniser extends Disposable {
case UserDataSyncErrorCode.LocalPreconditionFailed:
// Rejected as there is a new local version. Syncing again...
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`);
return this.performSync(remoteUserData, lastSyncUserData);
return this.performSync(remoteUserData, lastSyncUserData, apply);
case UserDataSyncErrorCode.PreconditionFailed:
// Rejected as there is a new remote version. Syncing again...
@@ -332,32 +336,28 @@ export abstract class AbstractSynchroniser extends Disposable {
// and one of them successfully updated remote and last sync state.
lastSyncUserData = await this.getLastSyncUserData();
return this.performSync(remoteUserData, lastSyncUserData);
return this.performSync(remoteUserData, lastSyncUserData, apply);
}
}
throw e;
}
}
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
try {
// generate or use existing preview
if (!this.syncPreviewPromise) {
this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
}
const preview = await this.syncPreviewPromise;
if (preview.hasConflicts) {
return SyncStatus.HasConflicts;
this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token));
}
// apply preview
await this.applyPreview(preview, false);
if (apply) {
const preview = await this.syncPreviewPromise;
const newConflicts = preview.resourcePreviews.filter(({ hasConflicts }) => hasConflicts);
return await this.updateConflictsAndApply(newConflicts, false);
} else {
return SyncStatus.Syncing;
}
// reset preview
this.syncPreviewPromise = null;
return SyncStatus.Idle;
} catch (error) {
// reset preview on error
@@ -367,32 +367,128 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
protected async getSyncPreviewInProgress(): Promise<ISyncResourcePreview | null> {
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
async acceptPreviewContent(resource: URI, content: string, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
if (!this.syncPreviewPromise) {
return null;
}
try {
this.syncHeaders = { ...headers };
const preview = await this.syncPreviewPromise;
this.syncPreviewPromise = createCancelablePromise(token => this.updateSyncResourcePreviewContent(preview, resource, content, token));
return this.merge(resource, force, headers);
} finally {
this.syncHeaders = {};
}
}
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
let preview = await this.getSyncPreviewInProgress();
if (!preview || !preview.hasConflicts) {
return;
async merge(resource: URI, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
if (!this.syncPreviewPromise) {
return null;
}
try {
this.syncHeaders = { ...headers };
const preview = await this.syncPreviewPromise;
const resourcePreview = preview.resourcePreviews.find(({ localResource, remoteResource, previewResource }) =>
isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
if (!resourcePreview) {
return preview;
}
this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token));
preview = await this.syncPreviewPromise;
/* mark merged */
resourcePreview.merged = true;
if (!preview.hasConflicts) {
/* Add or remove the preview from conflicts */
const newConflicts = [...this._conflicts];
const index = newConflicts.findIndex(({ localResource, remoteResource, previewResource }) =>
isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
if (resourcePreview.hasConflicts) {
if (newConflicts.indexOf(resourcePreview) === -1) {
newConflicts.push(resourcePreview);
}
} else {
if (index !== -1) {
newConflicts.splice(index, 1);
}
}
// apply preview
await this.applyPreview(preview, false);
const status = await this.updateConflictsAndApply(newConflicts, force);
this.setStatus(status);
return this.syncPreviewPromise;
// reset preview
this.syncPreviewPromise = null;
} finally {
this.syncHeaders = {};
}
}
this.setStatus(SyncStatus.Idle);
private async updateConflictsAndApply(conflicts: IMergableResourcePreview[], force: boolean): Promise<SyncStatus> {
if (!this.syncPreviewPromise) {
return SyncStatus.Idle;
}
const preview = await this.syncPreviewPromise;
// update conflicts
this.updateConflicts(conflicts);
if (this._conflicts.length) {
return SyncStatus.HasConflicts;
}
// check if all are merged
if (preview.resourcePreviews.some(r => !r.merged)) {
return SyncStatus.Syncing;
}
// apply preview
await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, force);
// reset preview
this.syncPreviewPromise = null;
// reset preview folder
await this.clearPreviewFolder();
return SyncStatus.Idle;
}
private async updateSyncResourcePreviewContent(preview: ISyncResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<ISyncResourcePreview> {
const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource, localChange, remoteChange }) =>
(localChange !== Change.None || remoteChange !== Change.None)
&& (isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource)));
if (index !== -1) {
const resourcePreviews = [...preview.resourcePreviews];
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], resource, previewContent, token);
resourcePreviews[index] = { ...resourcePreview, merged: resourcePreviews[index].merged };
preview = {
...preview,
resourcePreviews
};
}
return preview;
}
protected async updateResourcePreviewContent(resourcePreview: IResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IResourcePreview> {
return {
...resourcePreview,
previewContent,
hasConflicts: false,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
private async clearPreviewFolder(): Promise<void> {
try {
await this.fileService.del(this.syncPreviewFolder, { recursive: true });
} catch (error) { /* Ignore */ }
}
private updateConflicts(conflicts: IMergableResourcePreview[]): void {
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(conflicts);
}
}
async hasPreviouslySynced(): Promise<boolean> {
@@ -400,11 +496,6 @@ export abstract class AbstractSynchroniser extends Disposable {
return !!lastSyncData;
}
protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise<boolean> {
const machineId = await this.currentMachineIdPromise;
return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
}
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
@@ -447,12 +538,47 @@ export abstract class AbstractSynchroniser extends Disposable {
return null;
}
protected async resolvePreviewContent(uri: URI): Promise<string | null> {
const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
if (syncPreview) {
for (const resourcePreview of syncPreview.resourcePreviews) {
if (resourcePreview.previewResource && isEqual(resourcePreview.previewResource, uri)) {
return resourcePreview.previewContent || '';
}
if (resourcePreview.remoteResource && isEqual(resourcePreview.remoteResource, uri)) {
return resourcePreview.remoteContent || '';
}
if (resourcePreview.localResource && isEqual(resourcePreview.localResource, uri)) {
return resourcePreview.localContent || '';
}
}
}
return null;
}
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncResource);
} catch (e) { /* ignore */ }
}
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, merge: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
const machineId = await this.currentMachineIdPromise;
const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
const resourcePreviews = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
// Mark merge
const mergableResourcePreviews = resourcePreviews.map(r => ({
...r,
merged: merge || (r.localChange === Change.None && r.remoteChange === Change.None) /* Mark previews with no changes as merged */
}));
return { remoteUserData, lastSyncUserData, resourcePreviews: mergableResourcePreviews, isLastSyncFromCurrentMachine };
}
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
try {
const content = await this.fileService.readFile(this.lastSyncResource);
@@ -500,7 +626,7 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (error) {
this.logService.error(error);
}
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.Incompatible, this.resource);
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
}
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
@@ -526,26 +652,33 @@ export abstract class AbstractSynchroniser extends Disposable {
}
async stop(): Promise<void> {
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
if (this.status === SyncStatus.Idle) {
return;
}
this.logService.trace(`${this.syncResourceLogLabel}: Stopping synchronizing ${this.resource.toLowerCase()}.`);
if (this.syncPreviewPromise) {
this.syncPreviewPromise.cancel();
this.syncPreviewPromise = null;
}
this.updateConflicts([]);
await this.clearPreviewFolder();
this.setStatus(SyncStatus.Idle);
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
}
protected abstract readonly version: number;
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncResourcePreview>;
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
protected abstract updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncResourcePreview>;
protected abstract applyPreview(preview: ISyncResourcePreview, forcePush: boolean): Promise<void>;
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]>;
protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise<void>;
}
export interface IFileSyncPreview extends ISyncResourcePreview {
export interface IFileResourcePreview extends IResourcePreview {
readonly fileContent: IFileContent | null;
readonly content: string | null;
}
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
@@ -568,28 +701,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
}
async stop(): Promise<void> {
await super.stop();
try {
await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ }
}
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
const syncPreview = await this.getSyncPreviewInProgress();
if (syncPreview) {
if (isEqual(this.remotePreviewResource, conflictResource)) {
return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null;
}
if (isEqual(this.localPreviewResource, conflictResource)) {
return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null;
}
}
}
return null;
}
protected async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.file);
@@ -598,14 +709,14 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
}
}
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null, force: boolean): Promise<void> {
try {
if (oldContent) {
// file exists already
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force });
}
} catch (e) {
if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) ||
@@ -624,8 +735,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
this.triggerLocalChange();
}
protected abstract readonly localPreviewResource: URI;
protected abstract readonly remotePreviewResource: URI;
}
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {

View File

@@ -3,10 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { values, keys } from 'vs/base/common/map';
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { startsWith } from 'vs/base/common/strings';
import { deepClone } from 'vs/base/common/objects';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -78,7 +76,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
for (const key of baseToRemote.removed.values()) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
@@ -86,7 +84,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}
// Remotely added extension
for (const key of values(baseToRemote.added)) {
for (const key of baseToRemote.added.values()) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
@@ -103,13 +101,13 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
for (const key of baseToRemote.updated.values()) {
// Update in local always
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
for (const key of baseToLocal.added.values()) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
@@ -117,7 +115,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
for (const key of baseToLocal.updated.values()) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
@@ -135,7 +133,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
for (const key of baseToLocal.removed.values()) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
// Remove only if it is an installed extension
@@ -156,8 +154,8 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : [];
const toKeys = keys(to).filter(key => !ignoredExtensions.has(key));
const fromKeys = from ? [...from.keys()].filter(key => !ignoredExtensions.has(key)) : [];
const toKeys = [...to.keys()].filter(key => !ignoredExtensions.has(key));
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
@@ -190,7 +188,7 @@ function massageOutgoingExtension(extension: ISyncExtension, key: string): ISync
const massagedExtension: ISyncExtension = {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined
},
};
if (extension.disabled) {
@@ -211,7 +209,7 @@ export function getIgnoredExtensions(installed: ILocalExtension[], configuration
const added: string[] = [], removed: string[] = [];
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
if (key.startsWith('-')) {
removed.push(key.substring(1));
} else {
added.push(key);

View File

@@ -5,7 +5,7 @@
import {
IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService,
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -14,8 +14,8 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge, getIgnoredExtensions, IMergeResult } from 'vs/platform/userDataSync/common/extensionsMerge';
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
@@ -25,9 +25,8 @@ import { compare } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
interface IExtensionsSyncPreview extends ISyncResourcePreview {
export interface IExtensionResourcePreview extends IResourcePreview {
readonly localExtensions: ISyncExtension[];
readonly lastSyncUserData: ILastSyncUserData | null;
readonly added: ISyncExtension[];
readonly removed: IExtensionIdentifier[];
readonly updated: ISyncExtension[];
@@ -41,7 +40,7 @@ interface ILastSyncUserData extends IRemoteUserData {
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` });
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` });
/*
Version 3 - Introduce installed property to skip installing built in extensions
*/
@@ -74,84 +73,49 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
() => undefined, 500)(() => this.triggerLocalChange()));
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
if (remoteUserData.syncData !== null) {
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
remoteUserData, lastSyncUserData,
added, removed, updated, remote, localExtensions, skippedExtensions: [],
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
resourcePreviews,
};
} else {
return {
remoteUserData, lastSyncUserData,
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
hasLocalChanged: false,
hasRemoteChanged: false,
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
resourcePreviews: [],
};
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const pullPreview = await this.getPullPreview(remoteExtensions);
return [pullPreview];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
isLastSyncFromCurrentMachine: false,
hasConflicts: false,
resourcePreviews
};
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const pushPreview = await this.getPushPreview(remoteExtensions);
return [pushPreview];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
const { added, removed, updated } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
return [{
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
localContent: this.format(localExtensions),
remoteResource: this.remotePreviewResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.localPreviewResource,
previewContent: null,
added,
removed,
updated,
remote: syncExtensions,
localExtensions,
skippedExtensions: [],
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: Change.Modified,
hasConflicts: false,
resourcePreviews
};
}];
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncExtensions: ISyncExtension[] | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData!);
}
} else {
lastSyncExtensions = await this.parseAndMigrateExtensions(lastSyncUserData.syncData!);
}
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null;
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
@@ -165,36 +129,34 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
return [{
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
localContent: this.format(localExtensions),
remoteResource: this.remotePreviewResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.localPreviewResource,
previewContent: null,
added,
removed,
updated,
remote,
skippedExtensions,
remoteUserData,
localExtensions,
lastSyncUserData,
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
isLastSyncFromCurrentMachine,
skippedExtensions,
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
hasConflicts: false,
resourcePreviews
};
}];
}
protected async updatePreviewWithConflict(preview: IExtensionsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IExtensionsSyncPreview> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], force: boolean): Promise<void> {
let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0];
protected async applyPreview({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreview, forcePush: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
}
if (hasLocalChanged) {
if (localChange !== Change.None) {
await this.backupLocal(JSON.stringify(localExtensions));
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
}
@@ -203,7 +165,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
const content = JSON.stringify(remote);
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`);
}
@@ -215,16 +177,88 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
}
private getResourcePreviews({ added, removed, updated, remote }: IMergeResult): IResourcePreview[] {
const hasLocalChanged = added.length > 0 || removed.length > 0 || updated.length > 0;
const hasRemoteChanged = remote !== null;
return [{
hasLocalChanged,
protected async updateResourcePreviewContent(resourcePreview: IExtensionResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IExtensionResourcePreview> {
if (isEqual(resource, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
return this.getPushPreview(remoteExtensions);
}
return {
...resourcePreview,
previewContent,
hasConflicts: false,
hasRemoteChanged,
localResouce: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
remoteResource: this.remotePreviewResource
}];
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
private async getPullPreview(remoteExtensions: ISyncExtension[] | null): Promise<IExtensionResourcePreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const localResource = ExtensionsSynchroniser.EXTENSIONS_DATA_URI;
const localContent = this.format(localExtensions);
const remoteResource = this.remotePreviewResource;
const previewResource = this.localPreviewResource;
const previewContent = null;
if (remoteExtensions !== null) {
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
localResource,
localContent,
remoteResource,
remoteContent: this.format(remoteExtensions),
previewResource,
previewContent,
added,
removed,
updated,
remote,
localExtensions,
skippedExtensions: [],
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
hasConflicts: false,
};
} else {
return {
localResource,
localContent,
remoteResource,
remoteContent: null,
previewResource,
previewContent,
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
localChange: Change.None,
remoteChange: Change.None,
hasConflicts: false,
};
}
}
private async getPushPreview(remoteExtensions: ISyncExtension[] | null): Promise<IExtensionResourcePreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
localContent: this.format(localExtensions),
remoteResource: this.remotePreviewResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.localPreviewResource,
previewContent: null,
added,
removed,
updated,
remote,
localExtensions,
skippedExtensions: [],
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
hasConflicts: false,
};
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
@@ -238,6 +272,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return this.format(localExtensions);
}
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
return this.resolvePreviewContent(uri);
}
let content = await super.resolveContent(uri);
if (content) {
return content;

View File

@@ -6,7 +6,6 @@
import * as objects from 'vs/base/common/objects';
import { IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
import { IStringDictionary } from 'vs/base/common/collections';
import { values } from 'vs/base/common/map';
import { IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
import { ILogService } from 'vs/platform/log/common/log';
@@ -35,7 +34,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
const skipped: string[] = [];
// Added in remote
for (const key of values(baseToRemote.added)) {
for (const key of baseToRemote.added.values()) {
const remoteValue = remoteStorage[key];
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
@@ -59,7 +58,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
}
// Updated in Remote
for (const key of values(baseToRemote.updated)) {
for (const key of baseToRemote.updated.values()) {
const remoteValue = remoteStorage[key];
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
@@ -79,7 +78,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
}
// Removed in remote
for (const key of values(baseToRemote.removed)) {
for (const key of baseToRemote.removed.values()) {
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
@@ -89,14 +88,14 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
}
// Added in local
for (const key of values(baseToLocal.added)) {
for (const key of baseToLocal.added.values()) {
if (!baseToRemote.added.has(key)) {
remote[key] = localStorage[key];
}
}
// Updated in local
for (const key of values(baseToLocal.updated)) {
for (const key of baseToLocal.updated.values()) {
if (baseToRemote.updated.has(key) || baseToRemote.removed.has(key)) {
continue;
}
@@ -110,7 +109,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
}
// Removed in local
for (const key of values(baseToLocal.removed)) {
for (const key of baseToLocal.removed.values()) {
// do not remove from remote if it is updated in remote
if (baseToRemote.updated.has(key)) {
continue;

View File

@@ -5,7 +5,7 @@
import {
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event';
@@ -14,9 +14,9 @@ import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
import { IFileService } from 'vs/platform/files/common/files';
import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/globalStateMerge';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
import { parse } from 'vs/base/common/json';
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
@@ -30,12 +30,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
const argvStoragePrefx = 'globalState.argv.';
const argvProperties: string[] = ['locale'];
interface IGlobalStateSyncPreview extends ISyncResourcePreview {
export interface IGlobalStateResourcePreview extends IResourcePreview {
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
readonly remote: IStringDictionary<IStorageValue> | null;
readonly skippedStorageKeys: string[];
readonly localUserData: IGlobalState;
readonly lastSyncUserData: ILastSyncUserData | null;
}
interface ILastSyncUserData extends IRemoteUserData {
@@ -44,7 +43,7 @@ interface ILastSyncUserData extends IRemoteUserData {
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` });
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/globalState.json` });
protected readonly version: number = 1;
private readonly localPreviewResource: URI = joinPath(this.syncPreviewFolder, 'globalState.json');
private readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
@@ -75,76 +74,44 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
const localGlobalState = await this.getLocalGlobalState();
if (remoteUserData.syncData !== null) {
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
const { local, remote, skipped } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
remoteUserData, lastSyncUserData,
local, remote, localUserData: localGlobalState, skippedStorageKeys: skipped,
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
resourcePreviews
};
} else {
return {
remoteUserData, lastSyncUserData,
local: { added: {}, removed: [], updated: {} }, remote: null, localUserData: localGlobalState, skippedStorageKeys: [],
hasLocalChanged: false,
hasRemoteChanged: false,
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
resourcePreviews: []
};
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null;
const pullPreview = await this.getPullPreview(remoteContent, lastSyncUserData?.skippedStorageKeys || []);
return [pullPreview];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
const localUserData = await this.getLocalGlobalState();
return {
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: [],
hasLocalChanged: false,
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
hasConflicts: false,
resourcePreviews: this.getResourcePreviews({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, skipped: [] })
};
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null;
const pushPreview = await this.getPushPreview(remoteContent);
return [pushPreview];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateSyncPreview> {
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateResourcePreview[]> {
const localUserData = await this.getLocalGlobalState();
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
const { local, skipped } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
return [{
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
localContent: this.format(localUserData),
remoteResource: this.remotePreviewResource,
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
previewResource: this.localPreviewResource,
previewContent: null,
local,
remote: syncGlobalState.storage,
localUserData,
skippedStorageKeys: skipped,
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
remoteChange: Change.Modified,
hasConflicts: false,
resourcePreviews: [],
};
}];
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncGlobalState: IGlobalState | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
}
} else {
lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
}
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
const localGloablState = await this.getLocalGlobalState();
@@ -156,30 +123,32 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
const mergeResult = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
const { local, remote, skipped } = mergeResult;
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
return {
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
return [{
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
localContent: this.format(localGloablState),
remoteResource: this.remotePreviewResource,
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
previewResource: this.localPreviewResource,
previewContent: null,
local,
remote,
localUserData: localGloablState,
skippedStorageKeys: skipped,
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
isLastSyncFromCurrentMachine,
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
hasConflicts: false,
resourcePreviews
};
}];
}
protected async updatePreviewWithConflict(preview: IGlobalStateSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], force: boolean): Promise<void> {
let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0];
protected async applyPreview({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalStateSyncPreview, forcePush: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
}
if (hasLocalChanged) {
if (localChange !== Change.None) {
// update local
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
await this.backupLocal(JSON.stringify(localUserData));
@@ -187,11 +156,11 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
}
if (hasRemoteChanged) {
if (remoteChange !== Change.None) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
const content = JSON.stringify(<IGlobalState>{ storage: remote });
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
}
@@ -203,16 +172,79 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
}
}
private getResourcePreviews({ local, remote }: IMergeResult): IResourcePreview[] {
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
const hasRemoteChanged = remote !== null;
return [{
hasLocalChanged,
protected async updateResourcePreviewContent(resourcePreview: IGlobalStateResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IGlobalStateResourcePreview> {
if (GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI, resource) {
return this.getPushPreview(resourcePreview.remoteContent);
}
if (this.remotePreviewResource, resource) {
return this.getPullPreview(resourcePreview.remoteContent, resourcePreview.skippedStorageKeys);
}
return resourcePreview;
}
private async getPullPreview(remoteContent: string | null, skippedStorageKeys: string[]): Promise<IGlobalStateResourcePreview> {
const localGlobalState = await this.getLocalGlobalState();
const localResource = GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI;
const localContent = this.format(localGlobalState);
const remoteResource = this.remotePreviewResource;
const previewResource = this.localPreviewResource;
const previewContent = null;
if (remoteContent !== null) {
const remoteGlobalState: IGlobalState = JSON.parse(remoteContent);
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), skippedStorageKeys, this.logService);
const { local, remote, skipped } = mergeResult;
return {
localResource,
localContent,
remoteResource,
remoteContent: this.format(remoteGlobalState),
previewResource,
previewContent,
local,
remote,
localUserData: localGlobalState,
skippedStorageKeys: skipped,
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
hasConflicts: false,
};
} else {
return {
localResource,
localContent,
remoteResource,
remoteContent: null,
previewResource,
previewContent,
local: { added: {}, removed: [], updated: {} },
remote: null,
localUserData: localGlobalState,
skippedStorageKeys: [],
localChange: Change.None,
remoteChange: Change.None,
hasConflicts: false,
};
}
}
private async getPushPreview(remoteContent: string | null): Promise<IGlobalStateResourcePreview> {
const localUserData = await this.getLocalGlobalState();
const remoteGlobalState: IGlobalState = remoteContent ? JSON.parse(remoteContent) : null;
return {
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
localContent: this.format(localUserData),
remoteResource: this.remotePreviewResource,
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
previewResource: this.localPreviewResource,
previewContent: null,
local: { added: {}, removed: [], updated: {} },
remote: localUserData.storage,
localUserData,
skippedStorageKeys: [],
localChange: Change.None,
remoteChange: Change.Modified,
hasConflicts: false,
hasRemoteChanged,
localResouce: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
remoteResource: this.remotePreviewResource
}];
};
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
@@ -225,6 +257,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
return this.format(localGlobalState);
}
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
return this.resolvePreviewContent(uri);
}
let content = await super.resolveContent(uri);
if (content) {
return content;

View File

@@ -5,7 +5,6 @@
import * as objects from 'vs/base/common/objects';
import { parse } from 'vs/base/common/json';
import { values, keys } from 'vs/base/common/map';
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
import { firstIndex as findFirstIndex, equals } from 'vs/base/common/arrays';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
@@ -57,14 +56,14 @@ export async function merge(localContent: string, remoteContent: string, baseCon
const remoteByCommand = byCommand(remote);
const baseByCommand = base ? byCommand(base) : null;
const localToRemoteByCommand = compareByCommand(localByCommand, remoteByCommand, normalizedKeys);
const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: [...localByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: [...remoteByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand);
let mergeContent = localContent;
// Removed commands in Remote
for (const command of values(commandsMergeResult.removed)) {
for (const command of commandsMergeResult.removed.values()) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
@@ -72,7 +71,7 @@ export async function merge(localContent: string, remoteContent: string, baseCon
}
// Added commands in remote
for (const command of values(commandsMergeResult.added)) {
for (const command of commandsMergeResult.added.values()) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
@@ -86,7 +85,7 @@ export async function merge(localContent: string, remoteContent: string, baseCon
}
// Updated commands in Remote
for (const command of values(commandsMergeResult.updated)) {
for (const command of commandsMergeResult.updated.values()) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
@@ -109,7 +108,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
const conflicts: Set<string> = new Set<string>();
// Removed keys in Local
for (const key of values(baseToLocal.removed)) {
for (const key of baseToLocal.removed.values()) {
// Got updated in remote
if (baseToRemote.updated.has(key)) {
conflicts.add(key);
@@ -117,7 +116,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
}
// Removed keys in Remote
for (const key of values(baseToRemote.removed)) {
for (const key of baseToRemote.removed.values()) {
if (conflicts.has(key)) {
continue;
}
@@ -131,7 +130,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
}
// Added keys in Local
for (const key of values(baseToLocal.added)) {
for (const key of baseToLocal.added.values()) {
if (conflicts.has(key)) {
continue;
}
@@ -145,7 +144,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
}
// Added keys in remote
for (const key of values(baseToRemote.added)) {
for (const key of baseToRemote.added.values()) {
if (conflicts.has(key)) {
continue;
}
@@ -161,7 +160,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
}
// Updated keys in Local
for (const key of values(baseToLocal.updated)) {
for (const key of baseToLocal.updated.values()) {
if (conflicts.has(key)) {
continue;
}
@@ -175,7 +174,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
}
// Updated keys in Remote
for (const key of values(baseToRemote.updated)) {
for (const key of baseToRemote.updated.values()) {
if (conflicts.has(key)) {
continue;
}
@@ -204,13 +203,13 @@ function computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote
return { hasLocalForwarded: false, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
}
const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: keys(localByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: [...localByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
if (baseToLocalByKeybinding.added.size === 0 && baseToLocalByKeybinding.removed.size === 0 && baseToLocalByKeybinding.updated.size === 0) {
// Remote has moved forward and local has not.
return { hasLocalForwarded: false, hasRemoteForwarded: true, added: empty, removed: empty, updated: empty, conflicts: empty };
}
const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: keys(remoteByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: [...remoteByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
if (baseToRemoteByKeybinding.added.size === 0 && baseToRemoteByKeybinding.removed.size === 0 && baseToRemoteByKeybinding.updated.size === 0) {
return { hasLocalForwarded: true, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
}
@@ -250,8 +249,8 @@ function byCommand(keybindings: IUserFriendlyKeybinding[]): Map<string, IUserFri
function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): ICompareResult {
const fromKeys = keys(from);
const toKeys = keys(to);
const fromKeys = [...from.keys()];
const toKeys = [...to.keys()];
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
@@ -271,8 +270,8 @@ function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: M
}
function compareByCommand(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>, normalizedKeys: IStringDictionary<string>): ICompareResult {
const fromKeys = keys(from);
const toKeys = keys(to);
const fromKeys = [...from.keys()];
const toKeys = [...to.keys()];
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();

View File

@@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
import {
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
IRemoteUserData, ISyncData, IResourcePreview
IRemoteUserData, ISyncData, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { VSBuffer } from 'vs/base/common/buffer';
@@ -19,7 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
@@ -53,105 +53,69 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const hasLocalChanged = content !== null;
const hasRemoteChanged = false;
const hasConflicts = false;
const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
remoteResource: this.remotePreviewResource,
}];
return {
return [{
localResource: this.file,
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
isLastSyncFromCurrentMachine: false,
resourcePreviews
};
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
remoteContent: previewContent,
previewResource: this.localPreviewResource,
previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: Change.None,
hasConflicts: false,
}];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const content: string | null = fileContent ? fileContent.value.toString() : null;
const hasLocalChanged = false;
const hasRemoteChanged = content !== null;
const hasConflicts = false;
const previewContent: string | null = fileContent ? fileContent.value.toString() : null;
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
remoteResource: this.remotePreviewResource,
}];
return {
return [{
localResource: this.file,
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged,
hasRemoteChanged,
hasConflicts,
isLastSyncFromCurrentMachine: false,
resourcePreviews
};
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
previewResource: this.localPreviewResource,
previewContent,
localChange: Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
const hasLocalChanged = content !== null;
const hasRemoteChanged = content !== null;
const hasConflicts = false;
const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content);
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
remoteResource: this.remotePreviewResource,
}];
return {
return [{
localResource: this.file,
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
isLastSyncFromCurrentMachine: false,
resourcePreviews
};
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
previewResource: this.localPreviewResource,
previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncContent: string | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
}
} else {
lastSyncContent = lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
}
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
let content: string | null = null;
let previewContent: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
@@ -170,7 +134,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
// Sync only if there are changes
if (result.hasChanges) {
content = result.mergeContent;
previewContent = result.mergeContent;
hasConflicts = result.hasConflicts;
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
@@ -181,55 +145,49 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
content = fileContent.value.toString();
previewContent = fileContent.value.toString();
hasRemoteChanged = true;
}
if (content && !token.isCancellationRequested) {
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
if (previewContent && !token.isCancellationRequested) {
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
}
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
return [{
localResource: this.file,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
previewResource: this.localPreviewResource
remoteContent,
previewResource: this.localPreviewResource,
previewContent,
hasConflicts,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
}];
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
}
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
preview = { ...preview, content: conflictContent, hasConflicts: false };
}
return preview;
}
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0];
if (content !== null) {
if (this.hasErrors(content)) {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
if (hasLocalChanged) {
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
if (fileContent) {
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
}
await this.updateLocalFileContent(content, fileContent);
await this.updateLocalFileContent(content, fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
}
if (hasRemoteChanged) {
if (remoteChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null);
remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
}
@@ -272,7 +230,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(this.remotePreviewResource, uri)) {
if (isEqual(this.file, uri)) {
const fileContent = await this.getLocalFileContent();
return fileContent ? fileContent.value.toString() : '';
}
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
return this.resolvePreviewContent(uri);
}
let content = await super.resolveContent(uri);
@@ -292,11 +254,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return null;
}
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
const content = await super.resolvePreviewContent(conflictResource);
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
}
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);

View File

@@ -6,14 +6,12 @@
import * as objects from 'vs/base/common/objects';
import { parse, JSONVisitor, visit } from 'vs/base/common/json';
import { setProperty, withFormatting, applyEdits } from 'vs/base/common/jsonEdit';
import { values } from 'vs/base/common/map';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions, Edit, getEOL } from 'vs/base/common/jsonFormatter';
import * as contentUtil from 'vs/platform/userDataSync/common/content';
import { IConflictSetting, getDisallowedIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync';
import { firstIndex, distinct } from 'vs/base/common/arrays';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { startsWith } from 'vs/base/common/strings';
export interface IMergeResult {
localContent: string | null;
@@ -35,7 +33,7 @@ export function getIgnoredSettings(defaultIgnoredSettings: string[], configurati
const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()];
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
if (key.startsWith('-')) {
removed.push(key.substring(1));
} else {
added.push(key);
@@ -130,7 +128,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
};
// Removed settings in Local
for (const key of values(baseToLocal.removed)) {
for (const key of baseToLocal.removed.values()) {
// Conflict - Got updated in remote.
if (baseToRemote.updated.has(key)) {
handleConflict(key);
@@ -142,7 +140,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
}
// Removed settings in Remote
for (const key of values(baseToRemote.removed)) {
for (const key of baseToRemote.removed.values()) {
if (handledConflicts.has(key)) {
continue;
}
@@ -157,7 +155,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
}
// Updated settings in Local
for (const key of values(baseToLocal.updated)) {
for (const key of baseToLocal.updated.values()) {
if (handledConflicts.has(key)) {
continue;
}
@@ -173,7 +171,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
}
// Updated settings in Remote
for (const key of values(baseToRemote.updated)) {
for (const key of baseToRemote.updated.values()) {
if (handledConflicts.has(key)) {
continue;
}
@@ -189,7 +187,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
}
// Added settings in Local
for (const key of values(baseToLocal.added)) {
for (const key of baseToLocal.added.values()) {
if (handledConflicts.has(key)) {
continue;
}
@@ -205,7 +203,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
}
// Added settings in remote
for (const key of values(baseToRemote.added)) {
for (const key of baseToRemote.added.values()) {
if (handledConflicts.has(key)) {
continue;
}
@@ -223,7 +221,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
const hasConflicts = conflicts.size > 0 || !areSame(localContent, remoteContent, ignoredSettings);
const hasLocalChanged = hasConflicts || !areSame(localContent, originalLocalContent, []);
const hasRemoteChanged = hasConflicts || !areSame(remoteContent, originalRemoteContent, []);
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: values(conflicts), hasConflicts };
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: [...conflicts.values()], hasConflicts };
}
export function areSame(localContent: string, remoteContent: string, ignoredSettings: string[]): boolean {

View File

@@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
import {
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY,
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IUserDataSynchroniser,
IRemoteUserData, ISyncData, IResourcePreview
IRemoteUserData, ISyncData, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { localize } from 'vs/nls';
@@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -58,133 +58,95 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
let content: string | null = null;
let previewContent: string | null = null;
if (remoteSettingsSyncContent !== null) {
// Update ignored settings from local file content
content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
const hasLocalChanged = content !== null;
const hasRemoteChanged = false;
const hasConflicts = false;
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
remoteResource: this.remotePreviewResource,
}];
return {
return [{
localResource: this.file,
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged,
hasRemoteChanged,
hasConflicts,
isLastSyncFromCurrentMachine: false,
resourcePreviews
};
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.localPreviewResource,
previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: Change.None,
hasConflicts: false,
}];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
let content: string | null = null;
let previewContent: string | null = null;
if (fileContent !== null) {
// Remove ignored settings
content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
previewContent = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
}
const hasLocalChanged = false;
const hasRemoteChanged = content !== null;
const hasConflicts = false;
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
remoteResource: this.remotePreviewResource,
}];
return {
return [{
localResource: this.file,
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged,
hasRemoteChanged,
hasConflicts,
isLastSyncFromCurrentMachine: false,
resourcePreviews
};
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.localPreviewResource,
previewContent,
localChange: Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
let content: string | null = null;
let previewContent: string | null = null;
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
if (settingsSyncContent) {
content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
const hasLocalChanged = content !== null;
const hasRemoteChanged = content !== null;
const hasConflicts = false;
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
remoteResource: this.remotePreviewResource,
}];
return {
return [{
localResource: this.file,
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged,
hasRemoteChanged,
hasConflicts,
resourcePreviews,
isLastSyncFromCurrentMachine: false
};
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.localPreviewResource,
previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSettingsSyncContent: ISettingsSyncContent | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
}
} else {
lastSettingsSyncContent = this.getSettingsSyncContent(lastSyncUserData);
}
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
const ignoredSettings = await this.getIgnoredSettings();
let content: string | null = null;
let previewContent: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
@@ -193,9 +155,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
this.validateContent(localContent);
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
const ignoredSettings = await this.getIgnoredSettings();
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
content = result.localContent || result.remoteContent;
previewContent = result.localContent || result.remoteContent;
hasLocalChanged = result.localContent !== null;
hasRemoteChanged = result.remoteContent !== null;
hasConflicts = result.hasConflicts;
@@ -204,64 +165,61 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
content = fileContent.value.toString();
previewContent = fileContent.value.toString();
hasRemoteChanged = true;
}
if (content && !token.isCancellationRequested) {
if (previewContent && !token.isCancellationRequested) {
// Remove the ignored settings from the preview.
const ignoredSettings = await this.getIgnoredSettings();
const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
const content = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formattingOptions);
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
}
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
const resourcePreviews: IResourcePreview[] = [{
hasConflicts,
hasLocalChanged,
hasRemoteChanged,
localResouce: this.file,
return [{
localResource: this.file,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remotePreviewResource,
previewResource: this.localPreviewResource
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.localPreviewResource,
previewContent,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts,
}];
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
}
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
const formatUtils = await this.getFormattingOptions();
// Add ignored settings from local file content
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(conflictContent, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
preview = { ...preview, content, hasConflicts: false };
}
return preview;
protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IFileResourcePreview> {
const formatUtils = await this.getFormattingOptions();
// Add ignored settings from local file content
const ignoredSettings = await this.getIgnoredSettings();
previewContent = updateIgnoredSettings(previewContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
return super.updateResourcePreviewContent(resourcePreview, resource, previewContent, token) as Promise<IFileResourcePreview>;
}
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0];
if (content !== null) {
this.validateContent(content);
if (hasLocalChanged) {
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
}
await this.updateLocalFileContent(content, fileContent);
await this.updateLocalFileContent(content, fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
}
if (hasRemoteChanged) {
if (remoteChange !== Change.None) {
const formatUtils = await this.getFormattingOptions();
// Update ignored settings from remote
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const ignoredSettings = await this.getIgnoredSettings(content);
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), forcePush ? null : remoteUserData.ref);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
}
@@ -302,7 +260,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(this.remotePreviewResource, uri)) {
if (isEqual(this.file, uri)) {
const fileContent = await this.getLocalFileContent();
return fileContent ? fileContent.value.toString() : '';
}
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
return this.resolvePreviewContent(uri);
}
let content = await super.resolveContent(uri);
@@ -325,15 +287,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
return null;
}
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
let content = await super.resolvePreviewContent(conflictResource);
if (content !== null) {
const settingsSyncContent = this.parseSettingsSyncContent(content);
content = settingsSyncContent ? settingsSyncContent.settings : null;
}
protected async resolvePreviewContent(resource: URI): Promise<string | null> {
let content = await super.resolvePreviewContent(resource);
if (content !== null) {
const formatUtils = await this.getFormattingOptions();
// remove ignored settings from the remote content for preview
// remove ignored settings from the preview content
const ignoredSettings = await this.getIgnoredSettings();
content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils);
}

View File

@@ -3,30 +3,32 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { values } from 'vs/base/common/map';
import { IStringDictionary } from 'vs/base/common/collections';
import { deepClone } from 'vs/base/common/objects';
export interface IMergeResult {
added: IStringDictionary<string>;
updated: IStringDictionary<string>;
removed: string[];
local: {
added: IStringDictionary<string>;
updated: IStringDictionary<string>;
removed: string[];
};
remote: {
added: IStringDictionary<string>;
updated: IStringDictionary<string>;
removed: string[];
};
conflicts: string[];
remote: IStringDictionary<string> | null;
}
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null, resolvedConflicts: IStringDictionary<string | null> = {}): IMergeResult {
const added: IStringDictionary<string> = {};
const updated: IStringDictionary<string> = {};
const removed: Set<string> = new Set<string>();
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null): IMergeResult {
const localAdded: IStringDictionary<string> = {};
const localUpdated: IStringDictionary<string> = {};
const localRemoved: Set<string> = new Set<string>();
if (!remote) {
return {
added,
removed: values(removed),
updated,
conflicts: [],
remote: Object.keys(local).length > 0 ? local : null
local: { added: localAdded, updated: localUpdated, removed: [...localRemoved.values()] },
remote: { added: local, updated: {}, removed: [] },
conflicts: []
};
}
@@ -34,145 +36,118 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return {
added,
removed: values(removed),
updated,
conflicts: [],
remote: null
local: { added: localAdded, updated: localUpdated, removed: [...localRemoved.values()] },
remote: { added: {}, updated: {}, removed: [] },
conflicts: []
};
}
const baseToLocal = compare(base, local);
const baseToRemote = compare(base, remote);
const remoteContent: IStringDictionary<string> = deepClone(remote);
const remoteAdded: IStringDictionary<string> = {};
const remoteUpdated: IStringDictionary<string> = {};
const remoteRemoved: Set<string> = new Set<string>();
const conflicts: Set<string> = new Set<string>();
const handledConflicts: Set<string> = new Set<string>();
const handleConflict = (key: string): void => {
if (handledConflicts.has(key)) {
return;
}
handledConflicts.add(key);
const conflictContent = resolvedConflicts[key];
// add to conflicts
if (conflictContent === undefined) {
conflicts.add(key);
}
// remove the snippet
else if (conflictContent === null) {
delete remote[key];
if (local[key]) {
removed.add(key);
}
}
// add/update the snippet
else {
if (local[key]) {
if (local[key] !== conflictContent) {
updated[key] = conflictContent;
}
} else {
added[key] = conflictContent;
}
remoteContent[key] = conflictContent;
}
};
// Removed snippets in Local
for (const key of values(baseToLocal.removed)) {
for (const key of baseToLocal.removed.values()) {
// Conflict - Got updated in remote.
if (baseToRemote.updated.has(key)) {
// Add to local
added[key] = remote[key];
localAdded[key] = remote[key];
}
// Remove it in remote
else {
delete remoteContent[key];
remoteRemoved.add(key);
}
}
// Removed snippets in Remote
for (const key of values(baseToRemote.removed)) {
if (handledConflicts.has(key)) {
for (const key of baseToRemote.removed.values()) {
if (conflicts.has(key)) {
continue;
}
// Conflict - Got updated in local
if (baseToLocal.updated.has(key)) {
handleConflict(key);
conflicts.add(key);
}
// Also remove in Local
else {
removed.add(key);
localRemoved.add(key);
}
}
// Updated snippets in Local
for (const key of values(baseToLocal.updated)) {
if (handledConflicts.has(key)) {
for (const key of baseToLocal.updated.values()) {
if (conflicts.has(key)) {
continue;
}
// Got updated in remote
if (baseToRemote.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
conflicts.add(key);
}
} else {
remoteContent[key] = local[key];
remoteUpdated[key] = local[key];
}
}
// Updated snippets in Remote
for (const key of values(baseToRemote.updated)) {
if (handledConflicts.has(key)) {
for (const key of baseToRemote.updated.values()) {
if (conflicts.has(key)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
conflicts.add(key);
}
} else if (local[key] !== undefined) {
updated[key] = remote[key];
localUpdated[key] = remote[key];
}
}
// Added snippets in Local
for (const key of values(baseToLocal.added)) {
if (handledConflicts.has(key)) {
for (const key of baseToLocal.added.values()) {
if (conflicts.has(key)) {
continue;
}
// Got added in remote
if (baseToRemote.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
conflicts.add(key);
}
} else {
remoteContent[key] = local[key];
remoteAdded[key] = local[key];
}
}
// Added snippets in remote
for (const key of values(baseToRemote.added)) {
if (handledConflicts.has(key)) {
for (const key of baseToRemote.added.values()) {
if (conflicts.has(key)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
conflicts.add(key);
}
} else {
added[key] = remote[key];
localAdded[key] = remote[key];
}
}
return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent };
return {
local: { added: localAdded, removed: [...localRemoved.values()], updated: localUpdated },
remote: { added: remoteAdded, removed: [...remoteRemoved.values()], updated: remoteUpdated },
conflicts: [...conflicts.values()],
};
}
function compare(from: IStringDictionary<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
@@ -196,7 +171,7 @@ function compare(from: IStringDictionary<string> | null, to: IStringDictionary<s
return { added, removed, updated };
}
function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
export function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
const { added, removed, updated } = compare(a, b);
return added.size === 0 && removed.size === 0 && updated.size === 0;
}

View File

@@ -5,30 +5,22 @@
import {
IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService,
Conflict, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, IResourcePreview
USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, UserDataSyncError, UserDataSyncErrorCode, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStringDictionary } from 'vs/base/common/collections';
import { URI } from 'vs/base/common/uri';
import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename, dirname } from 'vs/base/common/resources';
import { joinPath, extname, relativePath, isEqualOrParent, basename, dirname } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer';
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/snippetsMerge';
import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface ISinppetsSyncPreview extends ISyncResourcePreview {
readonly local: IStringDictionary<IFileContent>;
readonly added: IStringDictionary<string>;
readonly updated: IStringDictionary<string>;
readonly removed: string[];
readonly conflicts: Conflict[];
readonly resolvedConflicts: IStringDictionary<string | null>;
readonly remote: IStringDictionary<string> | null;
}
import { deepClone } from 'vs/base/common/objects';
import { localize } from 'vs/nls';
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
@@ -60,83 +52,40 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
this.triggerLocalChange();
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const resourcePreviews: IFileResourcePreview[] = [];
if (remoteUserData.syncData !== null) {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
const mergeResult = merge(localSnippets, remoteSnippets, localSnippets);
const { added, updated, remote, removed } = mergeResult;
return {
remoteUserData, lastSyncUserData,
added, removed, updated, remote, local,
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: remote !== null,
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
isLastSyncFromCurrentMachine: false,
resourcePreviews: this.getResourcePreviews(mergeResult)
};
} else {
return {
remoteUserData, lastSyncUserData,
added: {}, removed: [], updated: {}, remote: null, local: {},
hasLocalChanged: false,
hasRemoteChanged: false,
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
isLastSyncFromCurrentMachine: false,
resourcePreviews: []
};
resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets));
}
return resourcePreviews;
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const mergeResult = merge(localSnippets, null, null);
const { added, updated, remote, removed } = mergeResult;
return {
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: remote !== null,
isLastSyncFromCurrentMachine: false,
hasConflicts: false,
resourcePreviews: this.getResourcePreviews(mergeResult)
};
const resourcePreviews = this.getResourcePreviews(mergeResult, local, {});
return resourcePreviews;
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreview> {
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const snippets = this.parseSnippets(syncData);
const mergeResult = merge(localSnippets, snippets, localSnippets);
const { added, updated, removed } = mergeResult;
return {
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasConflicts: false,
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
resourcePreviews: this.getResourcePreviews(mergeResult)
};
const resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets);
return resourcePreviews;
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const local = await this.getSnippetsFileContents();
return this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token);
}
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncSnippets: IStringDictionary<string> | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncSnippets = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
}
} else {
lastSyncSnippets = lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
}
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
if (remoteSnippets) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
@@ -144,79 +93,47 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
}
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
const resourcePreviews = this.getResourcePreviews(mergeResult);
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets);
const resourcePreviews = this.getResourcePreviews(mergeResult, local, remoteSnippets || {});
const conflicts: Conflict[] = [];
for (const key of mergeResult.conflicts) {
const localPreview = joinPath(this.syncPreviewFolder, key);
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
const content = local[key];
if (!token.isCancellationRequested) {
await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString(''));
}
}
for (const conflict of this.conflicts) {
// clear obsolete conflicts
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
try {
await this.fileService.del(conflict.local);
} catch (error) {
// Ignore & log
this.logService.error(error);
for (const resourcePreview of resourcePreviews) {
if (resourcePreview.hasConflicts) {
if (!token.isCancellationRequested) {
await this.fileService.writeFile(resourcePreview.previewResource!, VSBuffer.fromString(resourcePreview.previewContent || ''));
}
}
}
this.setConflicts(conflicts);
return resourcePreviews;
}
protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IFileResourcePreview> {
return {
remoteUserData, local,
lastSyncUserData,
added: mergeResult.added,
removed: mergeResult.removed,
updated: mergeResult.updated,
conflicts,
hasConflicts: conflicts.length > 0,
remote: mergeResult.remote,
resolvedConflicts,
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
hasRemoteChanged: mergeResult.remote !== null,
isLastSyncFromCurrentMachine,
resourcePreviews
...resourcePreview,
previewContent: previewContent || null,
hasConflicts: false,
localChange: previewContent ? Change.Modified : Change.Deleted,
remoteChange: previewContent ? Change.Modified : Change.Deleted,
};
}
protected async updatePreviewWithConflict(preview: ISinppetsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISinppetsSyncPreview> {
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
if (conflict) {
const key = relativePath(this.syncPreviewFolder, conflict.local)!;
preview.resolvedConflicts[key] = content || null;
preview = await this.doGeneratePreview(preview.local, preview.remoteUserData, preview.lastSyncUserData, preview.resolvedConflicts, token);
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
throw new UserDataSyncError(localize('unresolved conflicts', "Error while syncing {0}. Please resolve conflicts first.", this.syncResourceLogLabel), UserDataSyncErrorCode.UnresolvedConflicts, this.resource);
}
return preview;
}
protected async applyPreview(preview: ISinppetsSyncPreview, forcePush: boolean): Promise<void> {
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = preview;
if (!hasLocalChanged && !hasRemoteChanged) {
if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
}
if (hasLocalChanged) {
if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
// back up all snippets
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
await this.updateLocalSnippets(added, removed, updated, local);
await this.updateLocalBackup(resourcePreviews);
await this.updateLocalSnippets(resourcePreviews, force);
}
if (remote) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
const content = JSON.stringify(remote);
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, force);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
@@ -226,52 +143,149 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
}
for (const { previewResource } of resourcePreviews) {
// Delete the preview
try {
await this.fileService.del(previewResource);
} catch (e) { /* ignore */ }
}
}
private getResourcePreviews(mergeResult: IMergeResult): IResourcePreview[] {
const resourcePreviews: IResourcePreview[] = [];
for (const key of Object.keys(mergeResult.added)) {
resourcePreviews.push({
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: false
});
}
for (const key of Object.keys(mergeResult.updated)) {
resourcePreviews.push({
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
localResouce: joinPath(this.snippetsFolder, key),
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: true
});
}
for (const key of mergeResult.removed) {
resourcePreviews.push({
localResouce: joinPath(this.snippetsFolder, key),
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: false
});
}
for (const key of mergeResult.conflicts) {
resourcePreviews.push({
localResouce: joinPath(this.snippetsFolder, key),
private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): IFileResourcePreview[] {
const resourcePreviews: Map<string, IFileResourcePreview> = new Map<string, IFileResourcePreview>();
/* Snippets added remotely -> add locally */
for (const key of Object.keys(mergeResult.local.added)) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: null,
localContent: null,
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
hasConflicts: true,
hasLocalChanged: true,
hasRemoteChanged: true
previewContent: mergeResult.local.added[key],
hasConflicts: false,
localChange: Change.Added,
remoteChange: Change.None
});
}
return resourcePreviews;
}
/* Snippets updated remotely -> update locally */
for (const key of Object.keys(mergeResult.local.updated)) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: localFileContent[key],
localContent: localFileContent[key].value.toString(),
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.local.updated[key],
hasConflicts: false,
localChange: Change.Modified,
remoteChange: Change.None
});
}
async stop(): Promise<void> {
await this.clearConflicts();
return super.stop();
/* Snippets removed remotely -> remove locally */
for (const key of mergeResult.local.removed) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: localFileContent[key],
localContent: localFileContent[key].value.toString(),
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: null,
hasConflicts: false,
localChange: Change.Deleted,
remoteChange: Change.None
});
}
/* Snippets added locally -> add remotely */
for (const key of Object.keys(mergeResult.remote.added)) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: localFileContent[key],
localContent: localFileContent[key].value.toString(),
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.remote.added[key],
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Added
});
}
/* Snippets updated locally -> update remotely */
for (const key of Object.keys(mergeResult.remote.updated)) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: localFileContent[key],
localContent: localFileContent[key].value.toString(),
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.remote.updated[key],
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Modified
});
}
/* Snippets removed locally -> remove remotely */
for (const key of mergeResult.remote.removed) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: null,
localContent: null,
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: null,
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Deleted
});
}
/* Snippets with conflicts */
for (const key of mergeResult.conflicts) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: localFileContent[key] || null,
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: remoteSnippets[key] || null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
hasConflicts: true,
localChange: localFileContent[key] ? Change.Modified : Change.Added,
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
});
}
/* Unmodified Snippets */
for (const key of Object.keys(localFileContent)) {
if (!resourcePreviews.has(key)) {
resourcePreviews.set(key, {
localResource: joinPath(this.snippetsFolder, key),
fileContent: localFileContent[key] || null,
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
remoteContent: remoteSnippets[key] || null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.None
});
}
}
return [...resourcePreviews.values()];
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
@@ -294,13 +308,25 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.syncPreviewFolder)) {
if (isEqualOrParent(uri, this.snippetsFolder)) {
try {
const content = await this.fileService.readFile(uri);
return content ? content.value.toString() : null;
} catch (error) {
return '';
}
}
if (isEqualOrParent(uri.with({ scheme: this.syncPreviewFolder.scheme }), this.syncPreviewFolder)
|| isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME }))) {
return this.resolvePreviewContent(uri);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
@@ -309,20 +335,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return snippets[basename(uri)] || null;
}
}
return null;
}
private async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
const syncPreview = await this.getSyncPreviewInProgress();
if (syncPreview) {
const key = relativePath(this.syncPreviewFolder, conflictResource.with({ scheme: this.syncPreviewFolder.scheme }))!;
if (conflictResource.scheme === this.syncPreviewFolder.scheme) {
return (syncPreview as ISinppetsSyncPreview).local[key] ? (syncPreview as ISinppetsSyncPreview).local[key].value.toString() : null;
} else if (syncPreview.remoteUserData && syncPreview.remoteUserData.syncData) {
const snippets = this.parseSnippets(syncPreview.remoteUserData.syncData);
return snippets[key] || null;
}
}
return null;
}
@@ -338,34 +351,78 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return false;
}
private async clearConflicts(): Promise<void> {
if (this.conflicts.length) {
await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local)));
this.setConflicts([]);
private async updateLocalBackup(resourcePreviews: IFileResourcePreview[]): Promise<void> {
const local: IStringDictionary<IFileContent> = {};
for (const resourcePreview of resourcePreviews) {
if (resourcePreview.fileContent) {
local[basename(resourcePreview.localResource!)] = resourcePreview.fileContent;
}
}
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
}
private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
// Do not update if there are conflicts
return;
}
for (const { fileContent, previewContent: content, localResource, remoteResource, localChange } of resourcePreviews) {
if (localChange !== Change.None) {
const key = remoteResource ? basename(remoteResource) : basename(localResource!);
const resource = joinPath(this.snippetsFolder, key);
// Removed
if (localChange === Change.Deleted) {
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
await this.fileService.del(resource);
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
}
// Added
else if (localChange === Change.Added) {
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: force });
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
}
// Updated
else {
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
await this.fileService.writeFile(resource, VSBuffer.fromString(content!), force ? undefined : fileContent!);
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
}
}
}
}
private async updateLocalSnippets(added: IStringDictionary<string>, removed: string[], updated: IStringDictionary<string>, local: IStringDictionary<IFileContent>): Promise<void> {
for (const key of removed) {
const resource = joinPath(this.snippetsFolder, key);
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
await this.fileService.del(resource);
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
// Do not update if there are conflicts
return remoteUserData;
}
for (const key of Object.keys(added)) {
const resource = joinPath(this.snippetsFolder, key);
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false });
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
const currentSnippets: IStringDictionary<string> = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {};
const newSnippets: IStringDictionary<string> = deepClone(currentSnippets);
for (const { previewContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) {
if (remoteChange !== Change.None) {
const key = localResource ? basename(localResource) : basename(remoteResource!);
if (remoteChange === Change.Deleted) {
delete newSnippets[key];
} else {
newSnippets[key] = content!;
}
}
}
for (const key of Object.keys(updated)) {
const resource = joinPath(this.snippetsFolder, key);
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]);
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
if (!areSame(currentSnippets, newSnippets)) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(newSnippets), forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
}
return remoteUserData;
}
private parseSnippets(syncData: ISyncData): IStringDictionary<string> {

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { values } from 'vs/base/common/map';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -43,7 +42,7 @@ export class StorageKeysSyncRegistryService extends Disposable implements IStora
_serviceBrand: any;
private readonly _storageKeys = new Map<string, IStorageKey>();
get storageKeys(): ReadonlyArray<IStorageKey> { return values(this._storageKeys); }
get storageKeys(): ReadonlyArray<IStorageKey> { return [...this._storageKeys.values()]; }
private readonly _onDidChangeStorageKeys: Emitter<ReadonlyArray<IStorageKey>> = this._register(new Emitter<ReadonlyArray<IStorageKey>>());
readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event;

View File

@@ -6,7 +6,7 @@
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors';
@@ -81,12 +81,6 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
readonly onError: Event<UserDataSyncError> = this._onError.event;
private readonly _onTurnOnSync: Emitter<void> = this._register(new Emitter<void>());
readonly onTurnOnSync: Event<void> = this._onTurnOnSync.event;
private readonly _onDidTurnOnSync: Emitter<UserDataSyncError | undefined> = this._register(new Emitter<UserDataSyncError | undefined>());
readonly onDidTurnOnSync: Event<UserDataSyncError | undefined> = this._onDidTurnOnSync.event;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@@ -145,24 +139,9 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
return { enabled: true };
}
async turnOn(pullFirst: boolean): Promise<void> {
this._onTurnOnSync.fire();
try {
this.stopDisableMachineEventually();
if (pullFirst) {
await this.userDataSyncService.pull();
} else {
await this.userDataSyncService.sync();
}
this.setEnablement(true);
this._onDidTurnOnSync.fire(undefined);
} catch (error) {
this._onDidTurnOnSync.fire(error);
throw error;
}
async turnOn(): Promise<void> {
this.stopDisableMachineEventually();
this.setEnablement(true);
}
async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise<void> {
@@ -219,8 +198,14 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code });
}
// Turned off from another device or session got expired
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
// Session got expired
if (userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because current session is expired');
}
// Turned off from another device
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
}
@@ -233,6 +218,26 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
}
// Upgrade Required or Gone
else if (userDataSyncError.code === UserDataSyncErrorCode.UpgradeRequired || userDataSyncError.code === UserDataSyncErrorCode.Gone) {
await this.turnOff(false, true /* force soft turnoff on error */,
true /* do not disable machine because disabling a machine makes request to server and can fail with upgrade required or gone */);
this.disableMachineEventually();
this.logService.info('Auto Sync: Turned off sync because current client is not compatible with server. Requires client upgrade.');
}
// Incompatible Local Content
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleLocalContent) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because server has {0} content with newer version than of client. Requires client upgrade.', userDataSyncError.resource);
}
// Incompatible Remote Content
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleRemoteContent) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because server has {0} content with older version than of client. Requires server reset.', userDataSyncError.resource);
}
else {
this.logService.error(userDataSyncError);
this.successiveFailures++;
@@ -310,6 +315,7 @@ class AutoSync extends Disposable {
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
readonly onDidFinishSync = this._onDidFinishSync.event;
private syncTask: ISyncTask | undefined;
private syncPromise: CancelablePromise<void> | undefined;
constructor(
@@ -331,7 +337,9 @@ class AutoSync extends Disposable {
this.logService.info('Auto sync: Canelled sync that is in progress');
this.syncPromise = undefined;
}
this.userDataSyncService.stop();
if (this.syncTask) {
this.syncTask.stop();
}
this.logService.info('Auto Sync: Stopped');
}));
this.logService.info('Auto Sync: Started');
@@ -368,8 +376,11 @@ class AutoSync extends Disposable {
this._onDidStartSync.fire();
let error: Error | undefined;
try {
const syncTask = await this.userDataSyncService.createSyncTask();
let manifest = syncTask.manifest;
this.syncTask = await this.userDataSyncService.createSyncTask();
if (token.isCancellationRequested) {
return;
}
let manifest = this.syncTask.manifest;
// Server has no data but this machine was synced before
if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
@@ -396,7 +407,7 @@ class AutoSync extends Disposable {
throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
await syncTask.run(token);
await this.syncTask.run();
// After syncing, get the manifest if it was not available before
if (manifest === null) {

View File

@@ -23,7 +23,6 @@ import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/com
import { distinct } from 'vs/base/common/arrays';
import { isArray, isString, isObject } from 'vs/base/common/types';
import { IHeaders } from 'vs/base/parts/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
@@ -190,6 +189,13 @@ export interface IUserDataSyncBackupStoreService {
//#endregion
// #region User Data Sync Headers
export const HEADER_OPERATION_ID = 'x-operation-id';
export const HEADER_EXECUTION_ID = 'X-Execution-Id';
//#endregion
// #region User Data Sync Error
export enum UserDataSyncErrorCode {
@@ -211,34 +217,30 @@ export enum UserDataSyncErrorCode {
LocalPreconditionFailed = 'LocalPreconditionFailed',
LocalInvalidContent = 'LocalInvalidContent',
LocalError = 'LocalError',
Incompatible = 'Incompatible',
IncompatibleLocalContent = 'IncompatibleLocalContent',
IncompatibleRemoteContent = 'IncompatibleRemoteContent',
UnresolvedConflicts = 'UnresolvedConflicts',
Unknown = 'Unknown',
}
export class UserDataSyncError extends Error {
constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly resource?: SyncResource) {
constructor(
message: string,
readonly code: UserDataSyncErrorCode,
readonly resource?: SyncResource,
readonly operationId?: string
) {
super(message);
this.name = `${this.code} (UserDataSyncError) ${this.resource || ''}`;
}
static toUserDataSyncError(error: Error): UserDataSyncError {
if (error instanceof UserDataSyncError) {
return error;
}
const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name);
if (match && match[1]) {
return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], <SyncResource>match[2]);
}
return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
this.name = `${this.code} (UserDataSyncError) syncResource:${this.resource || 'unknown'} operationId:${this.operationId || 'unknown'}`;
}
}
export class UserDataSyncStoreError extends UserDataSyncError {
constructor(message: string, code: UserDataSyncErrorCode) {
super(message, code);
constructor(message: string, code: UserDataSyncErrorCode, readonly operationId: string | undefined) {
super(message, code, undefined, operationId);
}
}
@@ -248,6 +250,23 @@ export class UserDataAutoSyncError extends UserDataSyncError {
}
}
export namespace UserDataSyncError {
export function toUserDataSyncError(error: Error): UserDataSyncError {
if (error instanceof UserDataSyncError) {
return error;
}
const match = /^(.+) \(UserDataSyncError\) syncResource:(.+) operationId:(.+)$/.exec(error.name);
if (match && match[1]) {
const syncResource = match[2] === 'unknown' ? undefined : match[2] as SyncResource;
const operationId = match[3] === 'unknown' ? undefined : match[3];
return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], syncResource, operationId);
}
return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
}
}
//#endregion
// #region User Data Synchroniser
@@ -280,8 +299,6 @@ export interface ISyncResourceHandle {
uri: URI;
}
export type Conflict = { remote: URI, local: URI };
export interface IRemoteUserData {
ref: string;
syncData: ISyncData | null;
@@ -293,20 +310,24 @@ export interface ISyncData {
content: string;
}
export const enum Change {
None,
Added,
Modified,
Deleted,
}
export interface IResourcePreview {
readonly remoteResource?: URI;
readonly localResouce?: URI;
readonly previewResource?: URI;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
readonly remoteResource: URI;
readonly localResource: URI;
readonly previewResource: URI;
readonly localChange: Change;
readonly remoteChange: Change;
readonly merged: boolean;
}
export interface ISyncResourcePreview {
readonly isLastSyncFromCurrentMachine: boolean;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
readonly resourcePreviews: IResourcePreview[];
}
@@ -315,23 +336,26 @@ export interface IUserDataSynchroniser {
readonly resource: SyncResource;
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
readonly conflicts: Conflict[];
readonly onDidChangeConflicts: Event<Conflict[]>;
readonly conflicts: IResourcePreview[];
readonly onDidChangeConflicts: Event<IResourcePreview[]>;
readonly onDidChangeLocal: Event<void>;
pull(): Promise<void>;
push(): Promise<void>;
sync(manifest: IUserDataManifest | null, headers?: IHeaders): Promise<void>;
sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise<void>;
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
replace(uri: URI): Promise<boolean>;
stop(): Promise<void>;
generateSyncPreview(): Promise<ISyncResourcePreview | null>
hasPreviouslySynced(): Promise<boolean>
hasPreviouslySynced(): Promise<boolean>;
hasLocalData(): Promise<boolean>;
resetLocal(): Promise<void>;
resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>;
acceptPreviewContent(resource: URI, content: string, force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
merge(resource: URI, force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
@@ -352,11 +376,22 @@ export interface IUserDataSyncResourceEnablementService {
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
}
export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] };
export interface ISyncTask {
manifest: IUserDataManifest | null;
run(token: CancellationToken): Promise<void>;
readonly manifest: IUserDataManifest | null;
run(): Promise<void>;
stop(): Promise<void>;
}
export interface IManualSyncTask extends IDisposable {
readonly id: string;
readonly manifest: IUserDataManifest | null;
readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>;
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
accept(uri: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]>;
merge(uri?: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
pull(): Promise<void>;
push(): Promise<void>;
stop(): Promise<void>;
}
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
@@ -365,10 +400,9 @@ export interface IUserDataSyncService {
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
readonly onSynchronizeResource: Event<SyncResource>;
readonly conflicts: SyncResourceConflicts[];
readonly onDidChangeConflicts: Event<SyncResourceConflicts[]>;
readonly conflicts: [SyncResource, IResourcePreview[]][];
readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]>;
readonly onDidChangeLocal: Event<SyncResource>;
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>;
@@ -376,19 +410,19 @@ export interface IUserDataSyncService {
readonly lastSyncTime: number | undefined;
readonly onDidChangeLastSyncTime: Event<number>;
createSyncTask(): Promise<ISyncTask>;
createManualSyncTask(): Promise<IManualSyncTask>;
pull(): Promise<void>;
sync(): Promise<void>;
stop(): Promise<void>;
replace(uri: URI): Promise<void>;
reset(): Promise<void>;
resetRemote(): Promise<void>;
resetLocal(): Promise<void>;
createSyncTask(): Promise<ISyncTask>
isFirstTimeSyncingWithAnotherMachine(): Promise<boolean>;
hasLocalData(): Promise<boolean>;
hasPreviouslySynced(): Promise<boolean>;
resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>;
acceptPreviewContent(resource: SyncResource, conflictResource: URI, content: string): Promise<void>;
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
@@ -399,13 +433,11 @@ export interface IUserDataSyncService {
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
export interface IUserDataAutoSyncService {
_serviceBrand: any;
readonly onTurnOnSync: Event<void>
readonly onDidTurnOnSync: Event<UserDataSyncError | undefined>
readonly onError: Event<UserDataSyncError>;
readonly onDidChangeEnablement: Event<boolean>;
isEnabled(): boolean;
canToggleEnablement(): boolean;
turnOn(pullFirst: boolean): Promise<void>;
turnOn(): Promise<void>;
turnOff(everywhere: boolean): Promise<void>;
triggerSync(sources: string[], hasToLimitSync: boolean): Promise<void>;
}

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { URI } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
@@ -17,12 +17,11 @@ import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/use
export class UserDataSyncChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
constructor(private server: IPCServer, private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeStatus': return this.service.onDidChangeStatus;
case 'onSynchronizeResource': return this.service.onSynchronizeResource;
case 'onDidChangeConflicts': return this.service.onDidChangeConflicts;
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime;
@@ -44,15 +43,17 @@ export class UserDataSyncChannel implements IServerChannel {
private _call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
case 'createManualSyncTask': return this.createManualSyncTask();
case 'pull': return this.service.pull();
case 'sync': return this.service.sync();
case 'stop': this.service.stop(); return Promise.resolve();
case 'replace': return this.service.replace(URI.revive(args[0]));
case 'reset': return this.service.reset();
case 'resetRemote': return this.service.resetRemote();
case 'resetLocal': return this.service.resetLocal();
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine();
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
case 'hasLocalData': return this.service.hasLocalData();
case 'acceptPreviewContent': return this.service.acceptPreviewContent(args[0], URI.revive(args[1]), args[2]);
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
@@ -61,6 +62,39 @@ export class UserDataSyncChannel implements IServerChannel {
}
throw new Error('Invalid call');
}
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null }> {
const manualSyncTask = await this.service.createManualSyncTask();
const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask);
this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel);
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest };
}
}
class ManualSyncTaskChannel implements IServerChannel {
constructor(private readonly manualSyncTask: IManualSyncTask) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onSynchronizeResources': return this.manualSyncTask.onSynchronizeResources;
}
throw new Error(`Event not found: ${event}`);
}
async call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'preview': return this.manualSyncTask.preview();
case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]);
case 'merge': return this.manualSyncTask.merge(URI.revive(args[0]));
case 'pull': return this.manualSyncTask.pull();
case 'push': return this.manualSyncTask.push();
case 'stop': return this.manualSyncTask.stop();
case 'dispose': return this.manualSyncTask.dispose();
}
throw new Error('Invalid call');
}
}
export class UserDataAutoSyncChannel implements IServerChannel {
@@ -69,8 +103,6 @@ export class UserDataAutoSyncChannel implements IServerChannel {
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onTurnOnSync': return this.service.onTurnOnSync;
case 'onDidTurnOnSync': return this.service.onDidTurnOnSync;
case 'onError': return this.service.onError;
}
throw new Error(`Event not found: ${event}`);
@@ -79,7 +111,7 @@ export class UserDataAutoSyncChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'triggerSync': return this.service.triggerSync(args[0], args[1]);
case 'turnOn': return this.service.turnOn(args[0]);
case 'turnOn': return this.service.turnOn();
case 'turnOff': return this.service.turnOff(args[0]);
}
throw new Error('Invalid call');

View File

@@ -3,7 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
import {
IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode,
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID
} from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
@@ -21,6 +24,8 @@ import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSy
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders } from 'vs/base/parts/request/common/request';
import { generateUuid } from 'vs/base/common/uuid';
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
import { isPromiseCanceledError } from 'vs/base/common/errors';
type SyncErrorClassification = {
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -29,6 +34,12 @@ type SyncErrorClassification = {
const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
function createSyncHeaders(executionId: string): IHeaders {
const headers: IHeaders = {};
headers[HEADER_EXECUTION_ID] = executionId;
return headers;
}
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
_serviceBrand: any;
@@ -40,15 +51,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
private _onSynchronizeResource: Emitter<SyncResource> = this._register(new Emitter<SyncResource>());
readonly onSynchronizeResource: Event<SyncResource> = this._onSynchronizeResource.event;
readonly onDidChangeLocal: Event<SyncResource>;
private _conflicts: SyncResourceConflicts[] = [];
get conflicts(): SyncResourceConflicts[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<SyncResourceConflicts[]> = this._register(new Emitter<SyncResourceConflicts[]>());
readonly onDidChangeConflicts: Event<SyncResourceConflicts[]> = this._onDidChangeConflicts.event;
private _conflicts: [SyncResource, IResourcePreview[]][] = [];
get conflicts(): [SyncResource, IResourcePreview[]][] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<[SyncResource, IResourcePreview[]][]> = this._register(new Emitter<[SyncResource, IResourcePreview[]][]>());
readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]> = this._onDidChangeConflicts.event;
private _syncErrors: [SyncResource, UserDataSyncError][] = [];
private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>());
@@ -95,7 +103,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
try {
for (const synchroniser of this.synchronisers) {
try {
this._onSynchronizeResource.fire(synchroniser.resource);
await synchroniser.pull();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
@@ -129,18 +136,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
private recoveredSettings: boolean = false;
async sync(): Promise<void> {
const syncTask = await this.createSyncTask();
return syncTask.run(CancellationToken.None);
}
async createSyncTask(): Promise<ISyncTask> {
this.telemetryService.publicLog2('sync/getmanifest');
await this.checkEnablement();
const executionId = generateUuid();
let manifest: IUserDataManifest | null;
try {
manifest = await this.userDataSyncStoreService.manifest({ 'X-Execution-Id': executionId });
manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId));
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
@@ -150,20 +152,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
let executed = false;
const that = this;
let cancellablePromise: CancelablePromise<void> | undefined;
return {
manifest,
run(token: CancellationToken): Promise<void> {
run(): Promise<void> {
if (executed) {
throw new Error('Can run a task only once');
}
return that.doSync(manifest, executionId, token);
cancellablePromise = createCancelablePromise(token => that.sync(manifest, executionId, token));
return cancellablePromise.finally(() => cancellablePromise = undefined);
},
async stop(): Promise<void> {
if (cancellablePromise) {
cancellablePromise.cancel();
return that.stop();
}
}
};
}
private async doSync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
async createManualSyncTask(): Promise<IManualSyncTask> {
await this.checkEnablement();
const executionId = generateUuid();
const syncHeaders = createSyncHeaders(executionId);
let manifest: IUserDataManifest | null;
try {
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
}
throw error;
}
return new ManualSyncTask(executionId, manifest, syncHeaders, this.synchronisers, this.logService);
}
private recoveredSettings: boolean = false;
private async sync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
if (!this.recoveredSettings) {
await this.settingsSynchroniser.recoverSettings();
this.recoveredSettings = true;
@@ -182,7 +210,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.setStatus(SyncStatus.Syncing);
}
const syncHeaders: IHeaders = { 'X-Execution-Id': executionId };
const syncHeaders = createSyncHeaders(executionId);
for (const synchroniser of this.synchronisers) {
// Return if cancellation is requested
@@ -190,7 +218,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return;
}
try {
this._onSynchronizeResource.fire(synchroniser.resource);
await synchroniser.sync(manifest, syncHeaders);
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
@@ -211,18 +238,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
async replace(uri: URI): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.replace(uri)) {
return;
}
}
}
async stop(): Promise<void> {
await this.checkEnablement();
private async stop(): Promise<void> {
if (this.status === SyncStatus.Idle) {
return;
}
@@ -239,15 +255,21 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
async replace(uri: URI): Promise<void> {
await this.checkEnablement();
const syncResourceConflict = this.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(conflict, local) || isEqual(conflict, remote)))[0];
if (syncResourceConflict) {
const synchroniser = this.getSynchroniser(syncResourceConflict.syncResource);
await synchroniser.acceptConflict(conflict, content);
for (const synchroniser of this.synchronisers) {
if (await synchroniser.replace(uri)) {
return;
}
}
}
async acceptPreviewContent(syncResource: SyncResource, resource: URI, content: string, executionId: string = generateUuid()): Promise<void> {
await this.checkEnablement();
const synchroniser = this.getSynchroniser(syncResource);
await synchroniser.acceptPreviewContent(resource, content, false, createSyncHeaders(executionId));
}
async resolveContent(resource: URI): Promise<string | null> {
for (const synchroniser of this.synchronisers) {
const content = await synchroniser.resolveContent(resource);
@@ -274,35 +296,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
}
async isFirstTimeSyncingWithAnotherMachine(): Promise<boolean> {
await this.checkEnablement();
if (!await this.userDataSyncStoreService.manifest()) {
return false;
}
async hasLocalData(): Promise<boolean> {
// skip global state synchronizer
const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser];
let hasLocalData: boolean = false;
for (const synchroniser of synchronizers) {
if (await synchroniser.hasLocalData()) {
hasLocalData = true;
break;
}
}
if (!hasLocalData) {
return false;
}
for (const synchroniser of synchronizers) {
const preview = await synchroniser.generateSyncPreview();
if (preview && !preview.isLastSyncFromCurrentMachine && (preview.hasLocalChanged || preview.hasRemoteChanged)) {
return true;
}
}
return false;
}
@@ -312,6 +313,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
await this.resetLocal();
}
async resetRemote(): Promise<void> {
await this.checkEnablement();
try {
await this.userDataSyncStoreService.clear();
this.logService.info('Cleared data on server');
} catch (e) {
this.logService.error(e);
}
}
async resetLocal(): Promise<void> {
await this.checkEnablement();
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
@@ -335,16 +346,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return false;
}
private async resetRemote(): Promise<void> {
await this.checkEnablement();
try {
await this.userDataSyncStoreService.clear();
this.logService.info('Cleared data on server');
} catch (e) {
this.logService.error(e);
}
}
private setStatus(status: SyncStatus): void {
const oldStatus = this._status;
if (this._status !== status) {
@@ -364,7 +365,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private updateConflicts(): void {
const conflicts = this.computeConflicts();
if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote)))) {
if (!equals(this._conflicts, conflicts, ([syncResourceA, conflictsA], [syncResourceB, conflictsB]) => syncResourceA === syncResourceA && equals(conflictsA, conflictsB, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
this._conflicts = this.computeConflicts();
this._onDidChangeConflicts.fire(conflicts);
}
@@ -401,7 +402,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.Gone:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.Incompatible:
case UserDataSyncErrorCode.IncompatibleRemoteContent:
case UserDataSyncErrorCode.IncompatibleLocalContent:
throw e;
}
}
@@ -409,13 +411,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.error(`${source}: ${toErrorMessage(e)}`);
}
private computeConflicts(): SyncResourceConflicts[] {
private computeConflicts(): [SyncResource, IResourcePreview[]][] {
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
.map(s => ([s.resource, s.conflicts.map(toStrictResourcePreview)]));
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
return this.synchronisers.filter(s => s.resource === source)[0];
return this.synchronisers.find(s => s.resource === source)!;
}
private async checkEnablement(): Promise<void> {
@@ -425,3 +427,222 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
class ManualSyncTask extends Disposable implements IManualSyncTask {
private previewsPromise: CancelablePromise<[SyncResource, ISyncResourcePreview][]> | undefined;
private previews: [SyncResource, ISyncResourcePreview][] | undefined;
private synchronizingResources: [SyncResource, URI[]][] = [];
private _onSynchronizeResources = this._register(new Emitter<[SyncResource, URI[]][]>());
readonly onSynchronizeResources = this._onSynchronizeResources.event;
private isDisposed: boolean = false;
constructor(
readonly id: string,
readonly manifest: IUserDataManifest | null,
private readonly syncHeaders: IHeaders,
private readonly synchronisers: IUserDataSynchroniser[],
private readonly logService: IUserDataSyncLogService,
) {
super();
}
async preview(): Promise<[SyncResource, ISyncResourcePreview][]> {
if (this.isDisposed) {
throw new Error('Disposed');
}
if (!this.previewsPromise) {
this.previewsPromise = createCancelablePromise(token => this.getPreviews(token));
}
this.previews = await this.previewsPromise;
return this.previews;
}
async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.acceptPreviewContent(resource, content, force, this.syncHeaders));
}
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
if (resource) {
return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.merge(resource, force, this.syncHeaders));
} else {
return this.mergeAll();
}
}
private async mergeOrAccept(resource: URI, mergeOrAccept: (synchroniser: IUserDataSynchroniser, force: boolean) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('You need to create preview before merging or accepting');
}
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
if (index === -1) {
return this.previews;
}
const [syncResource, previews] = this.previews[index];
const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
if (!resourcePreview) {
return this.previews;
}
let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource);
if (!synchronizingResources) {
synchronizingResources = [syncResource, []];
this.synchronizingResources.push(synchronizingResources);
}
if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) {
synchronizingResources[1].push(resourcePreview.localResource);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
/* force only if the resource is local or remote resource */
const force = isEqual(resource, resourcePreview.localResource) || isEqual(resource, resourcePreview.remoteResource);
const preview = await mergeOrAccept(synchroniser, force);
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1);
if (!synchronizingResources[1].length) {
this.synchronizingResources.splice(i, 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
return this.previews;
}
private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('You need to create preview before merging');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot merge while synchronizing resources');
}
const previews: [SyncResource, ISyncResourcePreview][] = [];
for (const [syncResource, preview] of this.previews) {
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
this._onSynchronizeResources.fire(this.synchronizingResources);
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
let syncResourcePreview = null;
for (const resourcePreview of preview.resourcePreviews) {
syncResourcePreview = await synchroniser.merge(resourcePreview.remoteResource, false, this.syncHeaders);
}
if (syncResourcePreview) {
previews.push([syncResource, syncResourcePreview]);
}
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
this.previews = previews;
return this.previews;
}
async pull(): Promise<void> {
if (!this.previews) {
throw new Error('You need to create preview before applying');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot pull while synchronizing resources');
}
for (const [syncResource, preview] of this.previews) {
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
this._onSynchronizeResources.fire(this.synchronizingResources);
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
for (const resourcePreview of preview.resourcePreviews) {
const content = await synchroniser.resolveContent(resourcePreview.remoteResource) || '';
await synchroniser.acceptPreviewContent(resourcePreview.remoteResource, content, true, this.syncHeaders);
}
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
this.previews = [];
}
async push(): Promise<void> {
if (!this.previews) {
throw new Error('You need to create preview before applying');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot pull while synchronizing resources');
}
for (const [syncResource, preview] of this.previews) {
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
this._onSynchronizeResources.fire(this.synchronizingResources);
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
for (const resourcePreview of preview.resourcePreviews) {
const content = await synchroniser.resolveContent(resourcePreview.localResource) || '';
await synchroniser.acceptPreviewContent(resourcePreview.localResource, content, true, this.syncHeaders);
}
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
this.previews = [];
}
async stop(): Promise<void> {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.stop();
} catch (error) {
if (!isPromiseCanceledError(error)) {
this.logService.error(error);
}
}
}
this.reset();
}
private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> {
const result: [SyncResource, ISyncResourcePreview][] = [];
for (const synchroniser of this.synchronisers) {
if (token.isCancellationRequested) {
return [];
}
const preview = await synchroniser.preview(this.manifest, this.syncHeaders);
if (preview) {
result.push(this.toSyncResourcePreview(synchroniser.resource, preview));
}
}
return result;
}
private toSyncResourcePreview(syncResource: SyncResource, preview: ISyncResourcePreview): [SyncResource, ISyncResourcePreview] {
return [
syncResource,
{
isLastSyncFromCurrentMachine: preview.isLastSyncFromCurrentMachine,
resourcePreviews: preview.resourcePreviews.map(toStrictResourcePreview)
}
];
}
private reset(): void {
if (this.previewsPromise) {
this.previewsPromise.cancel();
this.previewsPromise = undefined;
}
this.previews = undefined;
this.synchronizingResources = [];
}
dispose(): void {
this.reset();
this.isDisposed = true;
}
}
function toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview {
return {
localResource: resourcePreview.localResource,
previewResource: resourcePreview.previewResource,
remoteResource: resourcePreview.remoteResource,
localChange: resourcePreview.localChange,
remoteChange: resourcePreview.remoteChange,
merged: resourcePreview.merged,
};
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
import { joinPath, relativePath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -65,7 +65,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
});
/* A requests session that limits requests per sessions */
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService);
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService);
}
setAuthToken(token: string, type: string): void {
@@ -83,7 +83,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const result = await asJson<{ url: string, created: number }[]>(context) || [];
@@ -102,7 +102,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const content = await asText(context);
@@ -120,7 +120,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
}
@@ -145,12 +145,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const ref = context.res.headers['etag'];
if (!ref) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
}
const content = await asText(context);
return { ref, content };
@@ -171,12 +171,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const newRef = context.res.headers['etag'];
if (!newRef) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
}
return newRef;
}
@@ -192,7 +192,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const manifest = await asJson<IUserDataManifest>(context);
@@ -227,7 +227,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
// clear cached session.
@@ -241,7 +241,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
if (!this.authToken) {
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized);
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, undefined);
}
const commonHeaders = await this.commonHeadersPromise;
@@ -258,40 +258,48 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
let context;
try {
context = await this.session.request(options, token);
this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode });
} catch (e) {
if (!(e instanceof UserDataSyncStoreError)) {
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused);
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, undefined);
}
this.logService.info('Request failed', options.url);
throw e;
}
const operationId = context.res.headers[HEADER_OPERATION_ID];
const requestInfo = { url: options.url, status: context.res.statusCode, 'execution-id': options.headers[HEADER_EXECUTION_ID], 'operation-id': operationId };
if (isSuccess(context)) {
this.logService.trace('Request succeeded', requestInfo);
} else {
this.logService.info('Request failed', requestInfo);
}
if (context.res.statusCode === 401) {
this.authToken = undefined;
this._onTokenFailed.fire();
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized);
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, operationId);
}
this._onTokenSucceed.fire();
if (context.res.statusCode === 410) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone, operationId);
}
if (context.res.statusCode === 412) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId);
}
if (context.res.statusCode === 413) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, operationId);
}
if (context.res.statusCode === 426) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired, operationId);
}
if (context.res.statusCode === 429) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests, operationId);
}
return context;
@@ -315,13 +323,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
export class RequestsSession {
private count: number = 0;
private requests: string[] = [];
private startTime: Date | undefined = undefined;
constructor(
private readonly limit: number,
private readonly interval: number, /* in ms */
private readonly requestService: IRequestService,
private readonly logService: IUserDataSyncLogService,
) { }
request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
@@ -329,12 +338,13 @@ export class RequestsSession {
this.reset();
}
if (this.count >= this.limit) {
throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests);
if (this.requests.length >= this.limit) {
this.logService.info('Too many requests', ...this.requests);
throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined);
}
this.startTime = this.startTime || new Date();
this.count++;
this.requests.push(options.url!);
return this.requestService.request(options, token);
}
@@ -344,7 +354,7 @@ export class RequestsSession {
}
private reset(): void {
this.count = 0;
this.requests = [];
this.startTime = undefined;
}

View File

@@ -116,11 +116,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when local and remote are same with multiple entries', async () => {
@@ -129,11 +131,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when local and remote are same with multiple entries in different order', async () => {
@@ -142,11 +146,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when local and remote are same with different base content', async () => {
@@ -156,11 +162,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, base);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when a new entry is added to remote', async () => {
@@ -169,11 +177,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when multiple new entries are added to remote', async () => {
@@ -182,11 +192,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, remote);
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, remote);
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when new entry is added to remote from base and local has not changed', async () => {
@@ -195,11 +207,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, local);
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when an entry is removed from remote from base and local has not changed', async () => {
@@ -208,11 +222,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, local);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, ['typescript.json']);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, ['typescript.json']);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when all entries are removed from base and local has not changed', async () => {
@@ -221,11 +237,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, local);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, ['html.json', 'typescript.json']);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, ['html.json', 'typescript.json']);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when an entry is updated in remote from base and local has not changed', async () => {
@@ -234,11 +252,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, local);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.equal(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
@@ -247,11 +267,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, local);
assert.deepEqual(actual.added, { 'c.json': cSnippet });
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.removed, ['typescript.json']);
assert.deepEqual(actual.local.added, { 'c.json': cSnippet });
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.local.removed, ['typescript.json']);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when a new entries are added to local', async () => {
@@ -260,11 +282,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, local);
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
@@ -273,11 +297,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, remote);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet });
assert.deepEqual(actual.remote.added, { 'html.json': htmlSnippet1, 'c.json': cSnippet });
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when an entry is removed from local from base and remote has not changed', async () => {
@@ -286,11 +312,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, remote);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, local);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, ['typescript.json']);
});
test('merge when an entry is updated in local from base and remote has not changed', async () => {
@@ -299,11 +327,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, remote);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, local);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.remote.removed, []);
});
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
@@ -312,11 +342,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, remote);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, local);
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.remote.removed, ['typescript.json']);
});
test('merge when local and remote with one entry but different value', async () => {
@@ -325,11 +357,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, null);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, ['html.json']);
assert.deepEqual(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
@@ -339,11 +373,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, base);
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, ['html.json']);
assert.deepEqual(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge with single entry and local is empty', async () => {
@@ -353,11 +389,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, base);
assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, { 'html.json': htmlSnippet2 });
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, null);
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when local and remote has moved forwareded with conflicts', async () => {
@@ -367,41 +405,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, base);
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet2 });
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, ['html.json']);
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
});
test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => {
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
const remote = { 'typescript.json': tsSnippet2 };
const resolvedConflicts = { 'html.json': htmlSnippet2 };
const actual = merge(local, remote, base, resolvedConflicts);
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet });
});
test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => {
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
const remote = { 'typescript.json': tsSnippet2 };
const resolvedConflicts = { 'html.json': null };
const actual = merge(local, remote, base, resolvedConflicts);
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, ['html.json']);
assert.deepEqual(actual.conflicts, []);
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
test('merge when local and remote has moved forwareded with multiple conflicts', async () => {
@@ -411,26 +421,13 @@ suite('SnippetsMerge', () => {
const actual = merge(local, remote, base);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, {});
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.local.added, {});
assert.deepEqual(actual.local.updated, {});
assert.deepEqual(actual.local.removed, []);
assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']);
assert.deepEqual(actual.remote, null);
});
test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => {
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet };
const remote = { 'c.json': cSnippet };
const resolvedConflicts = { 'html.json': htmlSnippet1 };
const actual = merge(local, remote, base, resolvedConflicts);
assert.deepEqual(actual.added, {});
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 });
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.conflicts, ['typescript.json']);
assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 });
assert.deepEqual(actual.remote.added, {});
assert.deepEqual(actual.remote.updated, {});
assert.deepEqual(actual.remote.removed, []);
});
});

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, PREVIEW_DIR_NAME, ISyncData, IResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
@@ -12,8 +12,9 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { VSBuffer } from 'vs/base/common/buffer';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
import { joinPath } from 'vs/base/common/resources';
import { joinPath, dirname } from 'vs/base/common/resources';
import { IStringDictionary } from 'vs/base/common/collections';
import { URI } from 'vs/base/common/uri';
const tsSnippet1 = `{
@@ -276,7 +277,7 @@ suite('SnippetsSync', () => {
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertPreviews(testObject.conflicts, [local]);
});
test('first time sync when snippets exists - has conflicts and accept conflicts', async () => {
@@ -286,12 +287,10 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet1);
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet1, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(conflicts[0].local));
const actual1 = await readSnippet('html.json', testClient);
assert.equal(actual1, htmlSnippet1);
@@ -315,10 +314,7 @@ suite('SnippetsSync', () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
assertConflicts(testObject.conflicts, [
{ local: local1, remote: local1.with({ scheme: USER_DATA_SYNC_SCHEME }) },
{ local: local2, remote: local2.with({ scheme: USER_DATA_SYNC_SCHEME }) }
]);
assertPreviews(testObject.conflicts, [local1, local2]);
});
test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => {
@@ -331,15 +327,13 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(conflicts[0].local));
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
conflicts = testObject.conflicts;
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertPreviews(testObject.conflicts, [local]);
});
test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => {
@@ -352,14 +346,11 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
await testObject.acceptConflict(conflicts[1].local, tsSnippet1);
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
await testObject.acceptPreviewContent(conflicts[1].previewResource, tsSnippet1, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(conflicts[0].local));
assert.ok(!await fileService.exists(conflicts[1].local));
const actual1 = await readSnippet('html.json', testClient);
assert.equal(actual1, htmlSnippet2);
@@ -457,7 +448,7 @@ suite('SnippetsSync', () => {
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertPreviews(testObject.conflicts, [local]);
});
test('sync updating a snippet - resolve conflict', async () => {
@@ -470,7 +461,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet3, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2);
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet2, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -560,7 +551,7 @@ suite('SnippetsSync', () => {
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertPreviews(testObject.conflicts, [local]);
});
test('sync removing a snippet - resolve conflict', async () => {
@@ -574,7 +565,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3);
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet3, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -601,7 +592,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, '');
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, '', false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -689,6 +680,220 @@ suite('SnippetsSync', () => {
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'global.code-snippets': globalSnippet });
});
test('previews are reset after all conflicts resolved', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(dirname(conflicts[0].previewResource)));
});
test('merge when there are multiple snippets and only one snippet is merged', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
});
test('merge when there are multiple snippets and all snippets are merged', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
preview = await testObject.merge(preview!.resourcePreviews[1].localResource, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assert.deepEqual(testObject.conflicts, []);
});
test('merge when there are multiple snippets and one snippet has no changes and one snippet is merged', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet1, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assert.deepEqual(testObject.conflicts, []);
});
test('merge when there are multiple snippets with conflicts and only one snippet is merged', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
assert.equal(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assertPreviews(testObject.conflicts,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
]);
});
test('merge when there are multiple snippets with conflicts and all snippets are merged', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
preview = await testObject.merge(preview!.resourcePreviews[1].previewResource, false);
assert.equal(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assertPreviews(testObject.conflicts,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
});
test('accept when there are multiple snippets with conflicts and only one snippet is accepted', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, htmlSnippet2, false);
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
});
test('accept when there are multiple snippets with conflicts and all snippets are accepted', async () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
let preview = await testObject.preview(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
[
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, htmlSnippet2, false);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[1].previewResource, tsSnippet2, false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assert.deepEqual(testObject.conflicts, []);
});
function parseSnippets(content: string): IStringDictionary<string> {
const syncData: ISyncData = JSON.parse(content);
return JSON.parse(syncData.content);
@@ -719,8 +924,8 @@ suite('SnippetsSync', () => {
return null;
}
function assertConflicts(actual: Conflict[], expected: Conflict[]) {
assert.deepEqual(actual.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })), expected.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })));
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
}
});

View File

@@ -4,31 +4,37 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { Barrier } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
interface ITestSyncPreview extends ISyncResourcePreview {
ref?: string;
}
const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` });
class TestSynchroniser extends AbstractSynchroniser {
syncBarrier: Barrier = new Barrier();
syncResult: { hasConflicts: boolean, hasError: boolean } = { hasConflicts: false, hasError: false };
onDoSyncCall: Emitter<void> = this._register(new Emitter<void>());
failWhenGettingLatestRemoteUserData: boolean = false;
readonly resource: SyncResource = SyncResource.Settings;
protected readonly version: number = 1;
private cancelled: boolean = false;
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
protected getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (this.failWhenGettingLatestRemoteUserData) {
throw new Error();
}
return super.getLatestRemoteUserData(manifest, lastSyncUserData);
}
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
this.cancelled = false;
this.onDoSyncCall.fire();
await this.syncBarrier.wait();
@@ -37,39 +43,35 @@ class TestSynchroniser extends AbstractSynchroniser {
return SyncStatus.Idle;
}
return super.doSync(remoteUserData, lastSyncUserData);
return super.doSync(remoteUserData, lastSyncUserData, apply);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ITestSyncPreview> {
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]> {
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
if (this.syncResult.hasError) {
throw new Error('failed');
}
return { ref: remoteUserData.ref, hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, conflictContent: string): Promise<ISyncResourcePreview> {
return preview;
}
protected async applyPreview({ ref }: ITestSyncPreview, forcePush: boolean): Promise<void> {
if (ref) {
await this.apply(ref);
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: IResourcePreview[], forcePush: boolean): Promise<void> {
if (preview[0]?.previewContent) {
await this.applyRef(preview[0].previewContent);
}
}
async apply(ref: string): Promise<void> {
async applyRef(ref: string): Promise<void> {
const remoteUserData = await this.updateRemoteUserData('', ref);
await this.updateLastSyncUserData(remoteUserData);
}
@@ -137,19 +139,6 @@ suite('TestSynchronizer', () => {
assert.deepEqual(testObject.status, SyncStatus.Idle);
});
test('status is set correctly when sync has conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
const actual: SyncStatus[] = [];
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
await testObject.sync(await client.manifest());
assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
});
test('status is set correctly when sync has errors', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasError: true, hasConflicts: false };
@@ -167,6 +156,17 @@ suite('TestSynchronizer', () => {
}
});
test('status is set to hasConflicts when asked to sync if there are conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertConflicts(testObject.conflicts, [resource]);
});
test('sync should not run if syncing already', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
const promise = Event.toPromise(testObject.onDoSyncCall.event);
@@ -181,7 +181,7 @@ suite('TestSynchronizer', () => {
assert.deepEqual(actual, []);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
testObject.stop();
await testObject.stop();
});
test('sync should not run if disabled', async () => {
@@ -221,7 +221,7 @@ suite('TestSynchronizer', () => {
// update remote data before syncing so that 412 is thrown by server
const disposable = testObject.onDoSyncCall.event(async () => {
disposable.dispose();
await testObject.apply(ref);
await testObject.applyRef(ref);
server.reset();
testObject.syncBarrier.open();
});
@@ -251,5 +251,115 @@ suite('TestSynchronizer', () => {
assert.deepEqual(server.requests, []);
});
test('status is reset when getting latest remote data fails', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.failWhenGettingLatestRemoteUserData = true;
try {
await testObject.sync(await client.manifest());
assert.fail('Should throw an error');
} catch (error) {
}
assert.equal(testObject.status, SyncStatus.Idle);
});
test('preview: status is set to syncing when asked for preview if there are no conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
const preview = await testObject.preview(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle after merging if there are no conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle and sync is applied after accepting when there are no conflicts before merging', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!, false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to syncing when asked for preview if there are conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
const preview = await testObject.preview(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to hasConflicts after merging', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
const preview = await testObject.preview(await client.manifest());
await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews, [resource]);
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].previewResource]);
});
test('preview: status is set to idle and sync is applied after accepting when there are conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!, false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle and sync is applied after accepting when there are conflicts before merging', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!, false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
});
function assertConflicts(actual: IResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
}
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
}
});

View File

@@ -37,7 +37,7 @@ suite('UserDataAutoSyncService', () => {
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
target.reset();
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
@@ -59,7 +59,7 @@ suite('UserDataAutoSyncService', () => {
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
target.reset();
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
@@ -85,7 +85,7 @@ suite('UserDataAutoSyncService', () => {
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
target.reset();
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
@@ -107,7 +107,7 @@ suite('UserDataAutoSyncService', () => {
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
target.reset();
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
@@ -245,7 +245,7 @@ suite('UserDataAutoSyncService', () => {
// Set up and sync from the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Set up and sync from the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -334,7 +334,7 @@ suite('UserDataAutoSyncService', () => {
// Set up and sync from the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Set up and sync from the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -346,7 +346,7 @@ suite('UserDataAutoSyncService', () => {
await client.instantiationService.get(IUserDataSyncService).reset();
// Sync again from the first client to create new session
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Sync from the test client
target.reset();
@@ -383,5 +383,4 @@ suite('UserDataAutoSyncService', () => {
assert.deepEqual((<UserDataSyncStoreError>e).code, UserDataSyncErrorCode.TooManyRequests);
});
});

View File

@@ -120,8 +120,8 @@ export class UserDataSyncClient extends Disposable {
await configurationService.reloadConfiguration();
}
sync(): Promise<void> {
return this.instantiationService.get(IUserDataSyncService).sync();
async sync(): Promise<void> {
await (await this.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
}
read(resource: SyncResource): Promise<IUserData> {

View File

@@ -11,7 +11,6 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing tests
@@ -27,7 +26,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const testObject = client.instantiationService.get(IUserDataSyncService);
// Sync for first time
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
// Manifest
@@ -58,7 +57,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const testObject = client.instantiationService.get(IUserDataSyncService);
// Sync for first time
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
// Manifest
@@ -83,7 +82,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -92,17 +91,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Sync (pull) from the test client
target.reset();
await testObject.isFirstTimeSyncingWithAnotherMachine();
await testObject.pull();
assert.deepEqual(target.requests, [
/* first time sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
/* pull */
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
@@ -118,7 +109,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client with changes
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -132,14 +123,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Sync (pull) from the test client
target.reset();
await testObject.isFirstTimeSyncingWithAnotherMachine();
await testObject.pull();
assert.deepEqual(target.requests, [
/* first time sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
/* pull */
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
@@ -155,7 +141,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -164,17 +150,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Sync (merge) from the test client
target.reset();
await testObject.isFirstTimeSyncingWithAnotherMachine();
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
/* first time sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
/* sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
@@ -191,7 +169,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client with changes
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -206,15 +184,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Sync (merge) from the test client
target.reset();
await testObject.isFirstTimeSyncingWithAnotherMachine();
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
/* first time sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
/* first time sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
@@ -235,11 +207,11 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
// sync from the client again
target.reset();
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
// Manifest
@@ -254,7 +226,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
target.reset();
// Do changes in the client
@@ -266,7 +238,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
// Sync from the client
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
// Manifest
@@ -288,13 +260,13 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Sync from first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Sync from test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
// Do changes in first client and sync
const fileService = client.instantiationService.get(IFileService);
@@ -303,11 +275,11 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{ "a": "changed" }`));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Sync from test client
target.reset();
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
// Manifest
@@ -331,7 +303,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
// Reset from the client
target.reset();
@@ -351,14 +323,14 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
// Reset from the client
await testObject.reset();
// Sync again
target.reset();
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(target.requests, [
// Manifest
@@ -392,7 +364,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// sync from the client
const actualStatuses: SyncStatus[] = [];
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
await testObject.sync();
await (await testObject.createSyncTask()).run();
disposable.dispose();
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
@@ -407,7 +379,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
let fileService = client.instantiationService.get(IFileService);
let environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -418,10 +390,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const testObject = testClient.instantiationService.get(IUserDataSyncService);
// sync from the client
await testObject.sync();
await (await testObject.createSyncTask()).run();
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assert.deepEqual(testObject.conflicts.map(({ syncResource }) => syncResource), [SyncResource.Settings]);
assert.deepEqual(testObject.conflicts.map(([syncResource]) => syncResource), [SyncResource.Settings]);
});
test('test sync will sync other non conflicted areas', async () => {
@@ -433,7 +405,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
let fileService = client.instantiationService.get(IFileService);
let environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client and get conflicts in settings
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -442,17 +414,17 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
let testEnvironmentService = testClient.instantiationService.get(IEnvironmentService);
await testFileService.writeFile(testEnvironmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
// sync from the first client with changes in keybindings
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// sync from the test client
target.reset();
const actualStatuses: SyncStatus[] = [];
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
await testObject.sync();
await (await testObject.createSyncTask()).run();
disposable.dispose();
assert.deepEqual(actualStatuses, []);
@@ -475,7 +447,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
let fileService = client.instantiationService.get(IFileService);
let environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await client.instantiationService.get(IUserDataSyncService).sync();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
@@ -484,10 +456,11 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
environmentService = testClient.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// sync from the client
await testObject.stop();
const syncTask = (await testObject.createSyncTask());
syncTask.run();
await syncTask.stop();
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -500,7 +473,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
await testObject.sync();
await (await testObject.createSyncTask()).run();
for (const request of target.requestsWithAllHeaders) {
const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0;
@@ -517,10 +490,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
const testObject = client.instantiationService.get(IUserDataSyncService);
const syncTask = await testObject.createSyncTask();
await syncTask.run(CancellationToken.None);
await syncTask.run();
try {
await syncTask.run(CancellationToken.None);
await syncTask.run();
assert.fail('Should fail running the task again');
} catch (error) {
/* expected */

View File

@@ -14,6 +14,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { IRequestService } from 'vs/platform/request/common/request';
import { newWriteableBufferStream } from 'vs/base/common/buffer';
import { timeout } from 'vs/base/common/async';
import { NullLogService } from 'vs/platform/log/common/log';
suite('UserDataSyncStoreService', () => {
@@ -332,7 +333,7 @@ suite('UserDataSyncRequestsSession', () => {
};
test('too many requests are thrown when limit exceeded', async () => {
const testObject = new RequestsSession(1, 500, requestService);
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
await testObject.request({}, CancellationToken.None);
try {
@@ -346,14 +347,14 @@ suite('UserDataSyncRequestsSession', () => {
});
test('requests are handled after session is expired', async () => {
const testObject = new RequestsSession(1, 500, requestService);
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
await testObject.request({}, CancellationToken.None);
await timeout(600);
await testObject.request({}, CancellationToken.None);
});
test('too many requests are thrown after session is expired', async () => {
const testObject = new RequestsSession(1, 500, requestService);
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
await testObject.request({}, CancellationToken.None);
await timeout(600);
await testObject.request({}, CancellationToken.None);