Files
azuredatastudio/src/vs/platform/userDataSync/common/userDataSyncService.ts

701 lines
27 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode,
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change, IUserDataSyncStoreManagementService
} 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';
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { equals } from 'vs/base/common/arrays';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { isEqual } from 'vs/base/common/resources';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
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 = {
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
executionId?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
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;
private readonly synchronisers: IUserDataSynchroniser[];
private _status: SyncStatus = SyncStatus.Uninitialized;
get status(): SyncStatus { return this._status; }
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
readonly onDidChangeLocal: Event<SyncResource>;
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][]>());
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event;
private _lastSyncTime: number | undefined = undefined;
get lastSyncTime(): number | undefined { return this._lastSyncTime; }
private _onDidChangeLastSyncTime: Emitter<number> = this._register(new Emitter<number>());
readonly onDidChangeLastSyncTime: Event<number> = this._onDidChangeLastSyncTime.event;
private _onDidResetLocal = this._register(new Emitter<void>());
readonly onDidResetLocal = this._onDidResetLocal.event;
private _onDidResetRemote = this._register(new Emitter<void>());
readonly onDidResetRemote = this._onDidResetRemote.event;
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly snippetsSynchroniser: SnippetsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
private readonly globalStateSynchroniser: GlobalStateSynchroniser;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser));
this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser));
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
this.updateStatus();
if (this.userDataSyncStoreManagementService.userDataSyncStore) {
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts()));
}
this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined);
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource)));
}
async createSyncTask(): Promise<ISyncTask> {
await this.checkEnablement();
const executionId = generateUuid();
let manifest: IUserDataManifest | null;
try {
manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId));
} catch (error) {
error = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
throw error;
}
let executed = false;
const that = this;
let cancellablePromise: CancelablePromise<void> | undefined;
return {
manifest,
run(): Promise<void> {
if (executed) {
throw new Error('Can run a task only once');
}
cancellablePromise = createCancelablePromise(token => that.sync(manifest, executionId, token));
return cancellablePromise.finally(() => cancellablePromise = undefined);
},
async stop(): Promise<void> {
if (cancellablePromise) {
cancellablePromise.cancel();
return that.stop();
}
}
};
}
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) {
error = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
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;
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
const startTime = new Date().getTime();
this._syncErrors = [];
try {
this.logService.trace('Sync started.');
if (this.status !== SyncStatus.HasConflicts) {
this.setStatus(SyncStatus.Syncing);
}
const syncHeaders = createSyncHeaders(executionId);
for (const synchroniser of this.synchronisers) {
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
try {
await synchroniser.sync(manifest, syncHeaders);
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
}
}
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
this.updateLastSyncTime();
} catch (error) {
error = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
throw error;
} finally {
this.updateStatus();
this._onSyncErrors.fire(this._syncErrors);
}
}
private async stop(): Promise<void> {
if (this.status === SyncStatus.Idle) {
return;
}
for (const synchroniser of this.synchronisers) {
try {
if (synchroniser.status !== SyncStatus.Idle) {
await synchroniser.stop();
}
} catch (e) {
this.logService.error(e);
}
}
}
async replace(uri: URI): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.replace(uri)) {
return;
}
}
}
async accept(syncResource: SyncResource, resource: URI, content: string | null | undefined, apply: boolean): Promise<void> {
await this.checkEnablement();
const synchroniser = this.getSynchroniser(syncResource);
await synchroniser.accept(resource, content);
if (apply) {
await synchroniser.apply(false, createSyncHeaders(generateUuid()));
}
}
async resolveContent(resource: URI): Promise<string | null> {
for (const synchroniser of this.synchronisers) {
const content = await synchroniser.resolveContent(resource);
if (content) {
return content;
}
}
return null;
}
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]> {
return this.getSynchroniser(resource).getRemoteSyncResourceHandles();
}
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]> {
return this.getSynchroniser(resource).getLocalSyncResourceHandles();
}
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
}
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined> {
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
}
async hasLocalData(): Promise<boolean> {
// skip global state synchronizer
const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser];
for (const synchroniser of synchronizers) {
if (await synchroniser.hasLocalData()) {
return true;
}
}
return false;
}
async reset(): Promise<void> {
await this.checkEnablement();
await this.resetRemote();
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);
}
this._onDidResetRemote.fire();
}
async resetLocal(): Promise<void> {
await this.checkEnablement();
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.resetLocal();
} catch (e) {
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
this.logService.error(e);
}
}
this._onDidResetLocal.fire();
this.logService.info('Did reset the local sync state.');
}
async hasPreviouslySynced(): Promise<boolean> {
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasPreviouslySynced()) {
return true;
}
}
return false;
}
private setStatus(status: SyncStatus): void {
const oldStatus = this._status;
if (this._status !== status) {
this._status = status;
this._onDidChangeStatus.fire(status);
if (oldStatus === SyncStatus.HasConflicts) {
this.updateLastSyncTime();
}
}
}
private updateStatus(): void {
this.updateConflicts();
const status = this.computeStatus();
this.setStatus(status);
}
private updateConflicts(): void {
const conflicts = this.computeConflicts();
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);
}
}
private computeStatus(): SyncStatus {
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
return SyncStatus.Uninitialized;
}
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
return SyncStatus.HasConflicts;
}
if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) {
return SyncStatus.Syncing;
}
return SyncStatus.Idle;
}
private updateLastSyncTime(): void {
if (this.status === SyncStatus.Idle) {
this._lastSyncTime = new Date().getTime();
this.storageService.store(LAST_SYNC_TIME_KEY, this._lastSyncTime, StorageScope.GLOBAL);
this._onDidChangeLastSyncTime.fire(this._lastSyncTime);
}
}
private handleSynchronizerError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.TooLarge:
throw new UserDataSyncError(e.message, e.code, source);
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter:
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.Gone:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.IncompatibleRemoteContent:
case UserDataSyncErrorCode.IncompatibleLocalContent:
throw e;
}
}
this.logService.error(e);
this.logService.error(`${source}: ${toErrorMessage(e)}`);
}
private computeConflicts(): [SyncResource, IResourcePreview[]][] {
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
.map(s => ([s.resource, s.conflicts.map(toStrictResourcePreview)]));
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
return this.synchronisers.find(s => s.resource === source)!;
}
private async checkEnablement(): Promise<void> {
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
throw new Error('Not enabled');
}
}
}
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;
get status(): SyncStatus {
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
return SyncStatus.HasConflicts;
}
if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) {
return SyncStatus.Syncing;
}
return SyncStatus.Idle;
}
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));
}
if (!this.previews) {
this.previews = await this.previewsPromise;
}
return this.previews;
}
async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.performAction(resource, sychronizer => sychronizer.accept(resource, content));
}
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
if (resource) {
return this.performAction(resource, sychronizer => sychronizer.merge(resource));
} else {
return this.mergeAll();
}
}
async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.performAction(resource, sychronizer => sychronizer.discard(resource));
}
async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('Missing preview. Create preview and try again.');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot discard while synchronizing resources');
}
const conflictResources: URI[] = [];
for (const [, syncResourcePreview] of this.previews) {
for (const resourcePreview of syncResourcePreview.resourcePreviews) {
if (resourcePreview.mergeState === MergeState.Conflict) {
conflictResources.push(resourcePreview.previewResource);
}
}
}
for (const resource of conflictResources) {
await this.discard(resource);
}
return this.previews;
}
async apply(): Promise<[SyncResource, ISyncResourcePreview][]> {
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');
}
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)!;
/* merge those which are not yet merged */
for (const resourcePreview of preview.resourcePreviews) {
if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) {
await synchroniser.merge(resourcePreview.previewResource);
}
}
/* apply */
const newPreview = await synchroniser.apply(false, this.syncHeaders);
if (newPreview) {
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
}
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) {
await synchroniser.accept(resourcePreview.remoteResource);
}
await synchroniser.apply(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) {
await synchroniser.accept(resourcePreview.localResource);
}
await synchroniser.apply(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 performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('Missing preview. Create preview and try again.');
}
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])!;
const preview = await action(synchroniser);
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 or applying');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot merge or apply 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)!;
/* merge those which are not yet merged */
let newPreview: ISyncResourcePreview | null = preview;
for (const resourcePreview of preview.resourcePreviews) {
if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) {
newPreview = await synchroniser.merge(resourcePreview.previewResource);
}
}
if (newPreview) {
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
}
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
this.previews = previews;
return this.previews;
}
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,
acceptedResource: resourcePreview.acceptedResource,
localChange: resourcePreview.localChange,
remoteChange: resourcePreview.remoteChange,
mergeState: resourcePreview.mergeState,
};
}