mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-15 02:48:30 -05:00
701 lines
27 KiB
TypeScript
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,
|
|
};
|
|
}
|