Skip to content

Commit e77fd88

Browse files
committed
Clean up sync code
- move generating and applying sync to abstract synchronizer
1 parent eb2f637 commit e77fd88

12 files changed

Lines changed: 291 additions & 381 deletions

src/vs/platform/userDataSync/common/abstractSynchronizer.ts

Lines changed: 120 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { Disposable } from 'vs/base/common/lifecycle';
77
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
88
import { VSBuffer } from 'vs/base/common/buffer';
99
import { URI } from 'vs/base/common/uri';
10-
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
10+
import {
11+
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
12+
IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreview, IUserDataManifest, ISyncData, IRemoteUserData
13+
} from 'vs/platform/userDataSync/common/userDataSync';
1114
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
1215
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
13-
import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
16+
import { CancelablePromise, RunOnceScheduler, createCancelablePromise } from 'vs/base/common/async';
1417
import { Emitter, Event } from 'vs/base/common/event';
1518
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
1619
import { ParseError, parse } from 'vs/base/common/json';
@@ -23,22 +26,12 @@ import { uppercaseFirstLetter } from 'vs/base/common/strings';
2326
import { equals } from 'vs/base/common/arrays';
2427
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
2528
import { IStorageService } from 'vs/platform/storage/common/storage';
29+
import { CancellationToken } from 'vs/base/common/cancellation';
2630

2731
type SyncSourceClassification = {
2832
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
2933
};
3034

31-
export interface IRemoteUserData {
32-
ref: string;
33-
syncData: ISyncData | null;
34-
}
35-
36-
export interface ISyncData {
37-
version: number;
38-
machineId?: string;
39-
content: string;
40-
}
41-
4235
function isSyncData(thing: any): thing is ISyncData {
4336
if (thing
4437
&& (thing.version !== undefined && typeof thing.version === 'number')
@@ -58,9 +51,10 @@ function isSyncData(thing: any): thing is ISyncData {
5851
return false;
5952
}
6053

61-
6254
export abstract class AbstractSynchroniser extends Disposable {
6355

56+
private syncPreviewPromise: CancelablePromise<ISyncPreview> | null = null;
57+
6458
protected readonly syncFolder: URI;
6559
private readonly currentMachineIdPromise: Promise<string>;
6660

@@ -100,18 +94,33 @@ export abstract class AbstractSynchroniser extends Disposable {
10094
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
10195
}
10296

97+
protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); }
98+
10399
protected async triggerLocalChange(): Promise<void> {
104100
if (this.isEnabled()) {
105101
this.localChangeTriggerScheduler.schedule();
106102
}
107103
}
108104

109105
protected async doTriggerLocalChange(): Promise<void> {
110-
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
111-
const lastSyncUserData = await this.getLastSyncUserData();
112-
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData)).hasRemoteChanged : true;
113-
if (hasRemoteChanged) {
114-
this._onDidChangeLocal.fire();
106+
107+
// Sync again if current status is in conflicts
108+
if (this.status === SyncStatus.HasConflicts) {
109+
this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`);
110+
const preview = await this.syncPreviewPromise!;
111+
this.syncPreviewPromise = null;
112+
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData);
113+
this.setStatus(status);
114+
}
115+
116+
// Check if local change causes remote change
117+
else {
118+
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
119+
const lastSyncUserData = await this.getLastSyncUserData();
120+
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true;
121+
if (hasRemoteChanged) {
122+
this._onDidChangeLocal.fire();
123+
}
115124
}
116125
}
117126

@@ -141,8 +150,6 @@ export abstract class AbstractSynchroniser extends Disposable {
141150
}
142151
}
143152

144-
protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); }
145-
146153
async sync(manifest: IUserDataManifest | null): Promise<void> {
147154
if (!this.isEnabled()) {
148155
if (this.status !== SyncStatus.Idle) {
@@ -168,7 +175,7 @@ export abstract class AbstractSynchroniser extends Disposable {
168175

169176
let status: SyncStatus = SyncStatus.Idle;
170177
try {
171-
status = await this.doSync(remoteUserData, lastSyncUserData);
178+
status = await this.performSync(remoteUserData, lastSyncUserData);
172179
if (status === SyncStatus.HasConflicts) {
173180
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
174181
} else if (status === SyncStatus.Idle) {
@@ -224,28 +231,33 @@ export abstract class AbstractSynchroniser extends Disposable {
224231
return this.getRemoteUserData(lastSyncUserData);
225232
}
226233

227-
async getSyncPreview(): Promise<ISyncPreviewResult> {
228-
if (!this.isEnabled()) {
229-
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false };
234+
async generateSyncPreview(): Promise<ISyncPreview | null> {
235+
if (this.isEnabled()) {
236+
const lastSyncUserData = await this.getLastSyncUserData();
237+
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
238+
return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
230239
}
231-
232-
const lastSyncUserData = await this.getLastSyncUserData();
233-
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
234-
return this.generatePreview(remoteUserData, lastSyncUserData);
240+
return null;
235241
}
236242

237-
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
243+
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
238244
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
239245
// current version is not compatible with cloud version
240246
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
241247
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);
242248
}
249+
243250
try {
244-
const status = await this.performSync(remoteUserData, lastSyncUserData);
245-
return status;
251+
return await this.doSync(remoteUserData, lastSyncUserData);
246252
} catch (e) {
247253
if (e instanceof UserDataSyncError) {
248254
switch (e.code) {
255+
256+
case UserDataSyncErrorCode.LocalPreconditionFailed:
257+
// Rejected as there is a new local version. Syncing again...
258+
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`);
259+
return this.performSync(remoteUserData, lastSyncUserData);
260+
249261
case UserDataSyncErrorCode.PreconditionFailed:
250262
// Rejected as there is a new remote version. Syncing again...
251263
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);
@@ -257,13 +269,69 @@ export abstract class AbstractSynchroniser extends Disposable {
257269
// and one of them successfully updated remote and last sync state.
258270
lastSyncUserData = await this.getLastSyncUserData();
259271

260-
return this.doSync(remoteUserData, lastSyncUserData);
272+
return this.performSync(remoteUserData, lastSyncUserData);
261273
}
262274
}
263275
throw e;
264276
}
265277
}
266278

279+
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
280+
try {
281+
// generate or use existing preview
282+
if (!this.syncPreviewPromise) {
283+
this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
284+
}
285+
const preview = await this.syncPreviewPromise;
286+
287+
if (preview.hasConflicts) {
288+
return SyncStatus.HasConflicts;
289+
}
290+
291+
// apply preview
292+
await this.applyPreview(preview);
293+
294+
// reset preview
295+
this.syncPreviewPromise = null;
296+
297+
return SyncStatus.Idle;
298+
} catch (error) {
299+
300+
// reset preview on error
301+
this.syncPreviewPromise = null;
302+
303+
throw error;
304+
}
305+
}
306+
307+
protected async getSyncPreviewInProgress(): Promise<ISyncPreview | null> {
308+
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
309+
}
310+
311+
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
312+
let preview = await this.getSyncPreviewInProgress();
313+
314+
if (!preview || !preview.hasConflicts) {
315+
return;
316+
}
317+
318+
319+
this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token));
320+
preview = await this.syncPreviewPromise;
321+
322+
if (!preview.hasConflicts) {
323+
324+
// apply preview
325+
await this.applyPreview(preview);
326+
327+
// reset preview
328+
this.syncPreviewPromise = null;
329+
330+
this.setStatus(SyncStatus.Idle);
331+
}
332+
333+
}
334+
267335
async hasPreviouslySynced(): Promise<boolean> {
268336
const lastSyncData = await this.getLastSyncUserData();
269337
return !!lastSyncData;
@@ -394,26 +462,29 @@ export abstract class AbstractSynchroniser extends Disposable {
394462
return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData));
395463
}
396464

397-
abstract stop(): Promise<void>;
465+
async stop(): Promise<void> {
466+
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
467+
if (this.syncPreviewPromise) {
468+
this.syncPreviewPromise.cancel();
469+
this.syncPreviewPromise = null;
470+
}
471+
this.setStatus(SyncStatus.Idle);
472+
}
398473

399474
protected abstract readonly version: number;
400-
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
401475
protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void>;
402-
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
476+
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
477+
protected abstract updatePreviewWithConflict(preview: ISyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncPreview>;
478+
protected abstract applyPreview(preview: ISyncPreview): Promise<void>;
403479
}
404480

405-
export interface IFileSyncPreviewResult extends ISyncPreviewResult {
481+
export interface IFileSyncPreview extends ISyncPreview {
406482
readonly fileContent: IFileContent | null;
407-
readonly remoteUserData: IRemoteUserData;
408-
readonly lastSyncUserData: IRemoteUserData | null;
409483
readonly content: string | null;
410-
readonly hasConflicts: boolean;
411484
}
412485

413486
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
414487

415-
protected syncPreviewResultPromise: CancelablePromise<IFileSyncPreviewResult> | null = null;
416-
417488
constructor(
418489
protected readonly file: URI,
419490
resource: SyncResource,
@@ -433,23 +504,21 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
433504
}
434505

435506
async stop(): Promise<void> {
436-
this.cancel();
437-
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
507+
await super.stop();
438508
try {
439509
await this.fileService.del(this.localPreviewResource);
440510
} catch (e) { /* ignore */ }
441-
this.setStatus(SyncStatus.Idle);
442511
}
443512

444513
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
445514
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
446-
if (this.syncPreviewResultPromise) {
447-
const result = await this.syncPreviewResultPromise;
515+
const syncPreview = await this.getSyncPreviewInProgress();
516+
if (syncPreview) {
448517
if (isEqual(this.remotePreviewResource, conflictResource)) {
449-
return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null;
518+
return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null;
450519
}
451520
if (isEqual(this.localPreviewResource, conflictResource)) {
452-
return result.fileContent ? result.fileContent.value.toString() : null;
521+
return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null;
453522
}
454523
}
455524
}
@@ -487,35 +556,12 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
487556
if (!e.contains(this.file)) {
488557
return;
489558
}
490-
491-
if (!this.isEnabled()) {
492-
return;
493-
}
494-
495-
// Sync again if local file has changed and current status is in conflicts
496-
if (this.status === SyncStatus.HasConflicts) {
497-
this.syncPreviewResultPromise?.then(result => {
498-
this.cancel();
499-
this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status));
500-
});
501-
}
502-
503-
// Otherwise fire change event
504-
else {
505-
this.triggerLocalChange();
506-
}
507-
508-
}
509-
510-
protected cancel(): void {
511-
if (this.syncPreviewResultPromise) {
512-
this.syncPreviewResultPromise.cancel();
513-
this.syncPreviewResultPromise = null;
514-
}
559+
this.triggerLocalChange();
515560
}
516561

517562
protected abstract readonly localPreviewResource: URI;
518563
protected abstract readonly remotePreviewResource: URI;
564+
protected abstract applyPreview(preview: IFileSyncPreview): Promise<void>;
519565
}
520566

521567
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {

0 commit comments

Comments
 (0)