@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
1,160 lines (1,159 loc) • 40.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import { equals } from '@sussudio/base/common/arrays.mjs';
import { createCancelablePromise, ThrottledDelayer } from '@sussudio/base/common/async.mjs';
import { VSBuffer } from '@sussudio/base/common/buffer.mjs';
import { CancellationToken } from '@sussudio/base/common/cancellation.mjs';
import { Emitter } from '@sussudio/base/common/event.mjs';
import { parse } from '@sussudio/base/common/json.mjs';
import { Disposable } from '@sussudio/base/common/lifecycle.mjs';
import { uppercaseFirstLetter } from '@sussudio/base/common/strings.mjs';
import { isUndefined } from '@sussudio/base/common/types.mjs';
import { localize } from 'vscode-nls.mjs';
import { IConfigurationService } from '../../configuration/common/configuration.mjs';
import { IEnvironmentService } from '../../environment/common/environment.mjs';
import { FileOperationError, IFileService, toFileOperationResult } from '../../files/common/files.mjs';
import { ILogService } from '../../log/common/log.mjs';
import { getServiceMachineId } from '../../externalServices/common/serviceMachineId.mjs';
import { IStorageService } from '../../storage/common/storage.mjs';
import { ITelemetryService } from '../../telemetry/common/telemetry.mjs';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.mjs';
import {
getLastSyncResourceUri,
IUserDataSyncBackupStoreService,
IUserDataSyncLogService,
IUserDataSyncEnablementService,
IUserDataSyncStoreService,
IUserDataSyncUtilService,
PREVIEW_DIR_NAME,
UserDataSyncError,
USER_DATA_SYNC_CONFIGURATION_SCOPE,
USER_DATA_SYNC_SCHEME,
getPathSegments,
} from './userDataSync.mjs';
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.mjs';
export function isRemoteUserData(thing) {
if (
thing &&
thing.ref !== undefined &&
typeof thing.ref === 'string' &&
thing.ref !== '' &&
thing.syncData !== undefined &&
(thing.syncData === null || isSyncData(thing.syncData))
) {
return true;
}
return false;
}
export function isSyncData(thing) {
if (
thing &&
thing.version !== undefined &&
typeof thing.version === 'number' &&
thing.content !== undefined &&
typeof thing.content === 'string'
) {
// backward compatibility
if (Object.keys(thing).length === 2) {
return true;
}
if (Object.keys(thing).length === 3 && thing.machineId !== undefined && typeof thing.machineId === 'string') {
return true;
}
}
return false;
}
export function getSyncResourceLogLabel(syncResource, profile) {
return `${uppercaseFirstLetter(syncResource)}${profile.isDefault ? '' : ` (${profile.name})`}`;
}
let AbstractSynchroniser = class AbstractSynchroniser extends Disposable {
syncResource;
collection;
fileService;
environmentService;
storageService;
userDataSyncStoreService;
userDataSyncBackupStoreService;
userDataSyncEnablementService;
telemetryService;
logService;
configurationService;
syncPreviewPromise = null;
syncFolder;
syncPreviewFolder;
extUri;
currentMachineIdPromise;
_status = 'idle' /* SyncStatus.Idle */;
get status() {
return this._status;
}
_onDidChangStatus = this._register(new Emitter());
onDidChangeStatus = this._onDidChangStatus.event;
_conflicts = [];
get conflicts() {
return { ...this.syncResource, conflicts: this._conflicts };
}
_onDidChangeConflicts = this._register(new Emitter());
onDidChangeConflicts = this._onDidChangeConflicts.event;
localChangeTriggerThrottler = new ThrottledDelayer(50);
_onDidChangeLocal = this._register(new Emitter());
onDidChangeLocal = this._onDidChangeLocal.event;
lastSyncResource;
lastSyncUserDataStateKey = `${this.collection ? `${this.collection}.` : ''}${
this.syncResource.syncResource
}.lastSyncUserData`;
hasSyncResourceStateVersionChanged = false;
syncResourceLogLabel;
syncHeaders = {};
resource = this.syncResource.syncResource;
constructor(
syncResource,
collection,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
userDataSyncEnablementService,
telemetryService,
logService,
configurationService,
uriIdentityService,
) {
super();
this.syncResource = syncResource;
this.collection = collection;
this.fileService = fileService;
this.environmentService = environmentService;
this.storageService = storageService;
this.userDataSyncStoreService = userDataSyncStoreService;
this.userDataSyncBackupStoreService = userDataSyncBackupStoreService;
this.userDataSyncEnablementService = userDataSyncEnablementService;
this.telemetryService = telemetryService;
this.logService = logService;
this.configurationService = configurationService;
this.syncResourceLogLabel = getSyncResourceLogLabel(syncResource.syncResource, syncResource.profile);
this.extUri = uriIdentityService.extUri;
this.syncFolder = this.extUri.joinPath(
environmentService.userDataSyncHome,
...getPathSegments(
syncResource.profile.isDefault ? undefined : syncResource.profile.id,
syncResource.syncResource,
),
);
this.syncPreviewFolder = this.extUri.joinPath(this.syncFolder, PREVIEW_DIR_NAME);
this.lastSyncResource = getLastSyncResourceUri(
syncResource.profile.isDefault ? undefined : syncResource.profile.id,
syncResource.syncResource,
environmentService,
this.extUri,
);
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
}
triggerLocalChange() {
this.localChangeTriggerThrottler.trigger(() => this.doTriggerLocalChange());
}
async doTriggerLocalChange() {
// Sync again if current status is in conflicts
if (this.status === 'hasConflicts' /* SyncStatus.HasConflicts */) {
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,
true,
this.getUserDataSyncConfiguration(),
);
this.setStatus(status);
}
// Check if local change causes remote change
else {
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
const lastSyncUserData = await this.getLastSyncUserData();
const hasRemoteChanged = lastSyncUserData ? await this.hasRemoteChanged(lastSyncUserData) : true;
if (hasRemoteChanged) {
this._onDidChangeLocal.fire();
}
}
}
setStatus(status) {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
}
async sync(manifest, headers = {}) {
await this._sync(manifest, true, this.getUserDataSyncConfiguration(), headers);
}
async preview(manifest, userDataSyncConfiguration, headers = {}) {
return this._sync(manifest, false, userDataSyncConfiguration, headers);
}
async apply(force, headers = {}) {
try {
this.syncHeaders = { ...headers };
const status = await this.doApply(force);
this.setStatus(status);
return this.syncPreviewPromise;
} finally {
this.syncHeaders = {};
}
}
async _sync(manifest, apply, userDataSyncConfiguration, headers) {
try {
this.syncHeaders = { ...headers };
if (this.status === 'hasConflicts' /* SyncStatus.HasConflicts */) {
this.logService.info(
`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`,
);
return this.syncPreviewPromise;
}
if (this.status === 'syncing' /* SyncStatus.Syncing */) {
this.logService.info(
`${
this.syncResourceLogLabel
}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`,
);
return this.syncPreviewPromise;
}
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
this.setStatus('syncing' /* SyncStatus.Syncing */);
let status = 'idle'; /* SyncStatus.Idle */
try {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
status = await this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration);
if (status === 'hasConflicts' /* SyncStatus.HasConflicts */) {
this.logService.info(
`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`,
);
} else if (status === 'idle' /* SyncStatus.Idle */) {
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
}
return this.syncPreviewPromise || null;
} finally {
this.setStatus(status);
}
} finally {
this.syncHeaders = {};
}
}
async replace(content) {
const syncData = this.parseSyncData(content);
if (!syncData) {
return false;
}
await this.stop();
try {
this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`);
this.setStatus('syncing' /* SyncStatus.Syncing */);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData);
/* use replace sync data */
const resourcePreviewResults = await this.generateSyncPreview(
{ ref: remoteUserData.ref, syncData },
lastSyncUserData,
isRemoteDataFromCurrentMachine,
this.getUserDataSyncConfiguration(),
CancellationToken.None,
);
const resourcePreviews = [];
for (const resourcePreviewResult of resourcePreviewResults) {
/* Accept remote resource */
const acceptResult = await this.getAcceptResult(
resourcePreviewResult,
resourcePreviewResult.remoteResource,
undefined,
CancellationToken.None,
);
/* compute remote change */
const { remoteChange } = await this.getAcceptResult(
resourcePreviewResult,
resourcePreviewResult.previewResource,
resourcePreviewResult.remoteContent,
CancellationToken.None,
);
resourcePreviews.push([
resourcePreviewResult,
{
...acceptResult,
remoteChange: remoteChange !== 0 /* Change.None */ ? remoteChange : 2 /* Change.Modified */,
},
]);
}
await this.applyResult(remoteUserData, lastSyncUserData, resourcePreviews, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus('idle' /* SyncStatus.Idle */);
}
return true;
}
async isRemoteDataFromCurrentMachine(remoteUserData) {
const machineId = await this.currentMachineIdPromise;
return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
}
async getLatestRemoteUserData(manifest, lastSyncUserData) {
if (lastSyncUserData) {
const latestRef = manifest ? manifest[this.resource] : undefined;
// Last time synced resource and latest resource on server are same
if (lastSyncUserData.ref === latestRef) {
return lastSyncUserData;
}
// There is no resource on server and last time it was synced with no resource
if (latestRef === undefined && lastSyncUserData.syncData === null) {
return lastSyncUserData;
}
}
return this.getRemoteUserData(lastSyncUserData);
}
async performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration) {
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
// current version is not compatible with cloud version
this.telemetryService.publicLog2('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,
),
'IncompatibleLocalContent' /* UserDataSyncErrorCode.IncompatibleLocalContent */,
this.resource,
);
}
try {
return await this.doSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration);
} catch (e) {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case 'LocalPreconditionFailed' /* 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, apply, userDataSyncConfiguration);
case 'Conflict' /* UserDataSyncErrorCode.Conflict */:
case 'PreconditionFailed' /* UserDataSyncErrorCode.PreconditionFailed */:
// Rejected as there is a new remote version. Syncing again...
this.logService.info(
`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`,
);
// Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624
remoteUserData = await this.getRemoteUserData(null);
// Get the latest last sync user data. Because multiple parallel syncs (in Web) could share same last sync data
// and one of them successfully updated remote and last sync state.
lastSyncUserData = await this.getLastSyncUserData();
return this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration);
}
}
throw e;
}
}
async doSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration) {
try {
const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData);
const acceptRemote =
!isRemoteDataFromCurrentMachine &&
lastSyncUserData === null &&
this.getStoredLastSyncUserDataStateContent() !== undefined;
const merge = apply && !acceptRemote;
// generate or use existing preview
if (!this.syncPreviewPromise) {
this.syncPreviewPromise = createCancelablePromise((token) =>
this.doGenerateSyncResourcePreview(
remoteUserData,
lastSyncUserData,
isRemoteDataFromCurrentMachine,
merge,
userDataSyncConfiguration,
token,
),
);
}
let preview = await this.syncPreviewPromise;
if (apply && acceptRemote) {
this.logService.info(
`${this.syncResourceLogLabel}: Accepting remote because it was synced before and the last sync data is not available.`,
);
for (const resourcePreview of preview.resourcePreviews) {
preview = (await this.accept(resourcePreview.remoteResource)) || preview;
}
}
this.updateConflicts(preview.resourcePreviews);
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === 'conflict' /* MergeState.Conflict */)) {
return 'hasConflicts' /* SyncStatus.HasConflicts */;
}
if (apply) {
return await this.doApply(false);
}
return 'syncing' /* SyncStatus.Syncing */;
} catch (error) {
// reset preview on error
this.syncPreviewPromise = null;
throw error;
}
}
async merge(resource) {
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
await this.fileService.writeFile(
resourcePreview.previewResource,
VSBuffer.fromString(mergeResult?.content || ''),
);
const acceptResult =
mergeResult && !mergeResult.hasConflicts
? await this.getAcceptResult(
resourcePreview,
resourcePreview.previewResource,
undefined,
CancellationToken.None,
)
: undefined;
resourcePreview.acceptResult = acceptResult;
resourcePreview.mergeState = mergeResult.hasConflicts
? 'conflict' /* MergeState.Conflict */
: acceptResult
? 'accepted' /* MergeState.Accepted */
: 'preview' /* MergeState.Preview */;
resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange;
resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange;
return resourcePreview;
});
return this.syncPreviewPromise;
}
async accept(resource, content) {
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None);
resourcePreview.acceptResult = acceptResult;
resourcePreview.mergeState = 'accepted' /* MergeState.Accepted */;
resourcePreview.localChange = acceptResult.localChange;
resourcePreview.remoteChange = acceptResult.remoteChange;
return resourcePreview;
});
return this.syncPreviewPromise;
}
async discard(resource) {
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult.content || ''));
resourcePreview.acceptResult = undefined;
resourcePreview.mergeState = 'preview' /* MergeState.Preview */;
resourcePreview.localChange = mergeResult.localChange;
resourcePreview.remoteChange = mergeResult.remoteChange;
return resourcePreview;
});
return this.syncPreviewPromise;
}
async updateSyncResourcePreview(resource, updateResourcePreview) {
if (!this.syncPreviewPromise) {
return;
}
let preview = await this.syncPreviewPromise;
const index = preview.resourcePreviews.findIndex(
({ localResource, remoteResource, previewResource }) =>
this.extUri.isEqual(localResource, resource) ||
this.extUri.isEqual(remoteResource, resource) ||
this.extUri.isEqual(previewResource, resource),
);
if (index === -1) {
return;
}
this.syncPreviewPromise = createCancelablePromise(async (token) => {
const resourcePreviews = [...preview.resourcePreviews];
resourcePreviews[index] = await updateResourcePreview(resourcePreviews[index]);
return {
...preview,
resourcePreviews,
};
});
preview = await this.syncPreviewPromise;
this.updateConflicts(preview.resourcePreviews);
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === 'conflict' /* MergeState.Conflict */)) {
this.setStatus('hasConflicts' /* SyncStatus.HasConflicts */);
} else {
this.setStatus('syncing' /* SyncStatus.Syncing */);
}
}
async doApply(force) {
if (!this.syncPreviewPromise) {
return 'idle' /* SyncStatus.Idle */;
}
const preview = await this.syncPreviewPromise;
// check for conflicts
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === 'conflict' /* MergeState.Conflict */)) {
return 'hasConflicts' /* SyncStatus.HasConflicts */;
}
// check if all are accepted
if (preview.resourcePreviews.some(({ mergeState }) => mergeState !== 'accepted' /* MergeState.Accepted */)) {
return 'syncing' /* SyncStatus.Syncing */;
}
// apply preview
await this.applyResult(
preview.remoteUserData,
preview.lastSyncUserData,
preview.resourcePreviews.map((resourcePreview) => [resourcePreview, resourcePreview.acceptResult]),
force,
);
// reset preview
this.syncPreviewPromise = null;
// reset preview folder
await this.clearPreviewFolder();
return 'idle' /* SyncStatus.Idle */;
}
async clearPreviewFolder() {
try {
await this.fileService.del(this.syncPreviewFolder, { recursive: true });
} catch (error) {
/* Ignore */
}
}
updateConflicts(resourcePreviews) {
const conflicts = resourcePreviews.filter(({ mergeState }) => mergeState === 'conflict' /* MergeState.Conflict */);
if (!equals(this._conflicts, conflicts, (a, b) => this.extUri.isEqual(a.previewResource, b.previewResource))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(this.conflicts);
}
}
async hasPreviouslySynced() {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData && lastSyncData.syncData !== null /* `null` sync data implies resource is not synced */;
}
async resolvePreviewContent(uri) {
const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
if (syncPreview) {
for (const resourcePreview of syncPreview.resourcePreviews) {
if (this.extUri.isEqual(resourcePreview.acceptedResource, uri)) {
return resourcePreview.acceptResult ? resourcePreview.acceptResult.content : null;
}
if (this.extUri.isEqual(resourcePreview.remoteResource, uri)) {
return resourcePreview.remoteContent;
}
if (this.extUri.isEqual(resourcePreview.localResource, uri)) {
return resourcePreview.localContent;
}
if (this.extUri.isEqual(resourcePreview.baseResource, uri)) {
return resourcePreview.baseContent;
}
}
}
return null;
}
async resetLocal() {
this.storageService.remove(this.lastSyncUserDataStateKey, -1 /* StorageScope.APPLICATION */);
try {
await this.fileService.del(this.lastSyncResource);
} catch (error) {
if (toFileOperationResult(error) !== 1 /* FileOperationResult.FILE_NOT_FOUND */) {
this.logService.error(error);
}
}
}
async doGenerateSyncResourcePreview(
remoteUserData,
lastSyncUserData,
isRemoteDataFromCurrentMachine,
merge,
userDataSyncConfiguration,
token,
) {
const resourcePreviewResults = await this.generateSyncPreview(
remoteUserData,
lastSyncUserData,
isRemoteDataFromCurrentMachine,
userDataSyncConfiguration,
token,
);
const resourcePreviews = [];
for (const resourcePreviewResult of resourcePreviewResults) {
const acceptedResource = resourcePreviewResult.previewResource.with({
scheme: USER_DATA_SYNC_SCHEME,
authority: 'accepted',
});
/* No change -> Accept */
if (
resourcePreviewResult.localChange === 0 /* Change.None */ &&
resourcePreviewResult.remoteChange === 0 /* Change.None */
) {
resourcePreviews.push({
...resourcePreviewResult,
acceptedResource,
acceptResult: { content: null, localChange: 0 /* Change.None */, remoteChange: 0 /* Change.None */ },
mergeState: 'accepted' /* MergeState.Accepted */,
});
} else {
/* Changed -> Apply ? (Merge ? Conflict | Accept) : Preview */
/* Merge */
const mergeResult = merge ? await this.getMergeResult(resourcePreviewResult, token) : undefined;
if (token.isCancellationRequested) {
break;
}
await this.fileService.writeFile(
resourcePreviewResult.previewResource,
VSBuffer.fromString(mergeResult?.content || ''),
);
/* Conflict | Accept */
const acceptResult =
mergeResult && !mergeResult.hasConflicts
? /* Accept if merged and there are no conflicts */
await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, undefined, token)
: undefined;
resourcePreviews.push({
...resourcePreviewResult,
acceptResult,
mergeState: mergeResult?.hasConflicts
? 'conflict' /* MergeState.Conflict */
: acceptResult
? 'accepted' /* MergeState.Accepted */
: 'preview' /* MergeState.Preview */,
localChange: acceptResult
? acceptResult.localChange
: mergeResult
? mergeResult.localChange
: resourcePreviewResult.localChange,
remoteChange: acceptResult
? acceptResult.remoteChange
: mergeResult
? mergeResult.remoteChange
: resourcePreviewResult.remoteChange,
});
}
}
return {
syncResource: this.resource,
profile: this.syncResource.profile,
remoteUserData,
lastSyncUserData,
resourcePreviews,
isLastSyncFromCurrentMachine: isRemoteDataFromCurrentMachine,
};
}
async getLastSyncUserData() {
let storedLastSyncUserDataStateContent = this.getStoredLastSyncUserDataStateContent();
if (!storedLastSyncUserDataStateContent) {
storedLastSyncUserDataStateContent = await this.migrateLastSyncUserData();
}
// Last Sync Data state does not exist
if (!storedLastSyncUserDataStateContent) {
this.logService.info(`${this.syncResourceLogLabel}: Last sync data state does not exist.`);
return null;
}
const lastSyncUserDataState = JSON.parse(storedLastSyncUserDataStateContent);
const resourceSyncStateVersion = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
this.hasSyncResourceStateVersionChanged =
!!lastSyncUserDataState.version &&
!!resourceSyncStateVersion &&
lastSyncUserDataState.version !== resourceSyncStateVersion;
if (this.hasSyncResourceStateVersionChanged) {
this.logService.info(
`${this.syncResourceLogLabel}: Reset last sync state because last sync state version ${lastSyncUserDataState.version} is not compatible with current sync state version ${resourceSyncStateVersion}.`,
);
await this.resetLocal();
return null;
}
let syncData = undefined;
// Get Last Sync Data from Local
let retrial = 1;
while (syncData === undefined && retrial++ < 6 /* Retry 5 times */) {
try {
const lastSyncStoredRemoteUserData = await this.readLastSyncStoredRemoteUserData();
if (lastSyncStoredRemoteUserData) {
if (lastSyncStoredRemoteUserData.ref === lastSyncUserDataState.ref) {
syncData = lastSyncStoredRemoteUserData.syncData;
} else {
this.logService.info(
`${this.syncResourceLogLabel}: Last sync data stored locally is not same as the last sync state.`,
);
}
}
break;
} catch (error) {
if (
error instanceof FileOperationError &&
error.fileOperationResult === 1 /* FileOperationResult.FILE_NOT_FOUND */
) {
this.logService.info(`${this.syncResourceLogLabel}: Last sync resource does not exist locally.`);
break;
} else if (error instanceof UserDataSyncError) {
throw error;
} else {
// log and retry
this.logService.error(error, retrial);
}
}
}
// Get Last Sync Data from Remote
if (syncData === undefined) {
try {
const content = await this.userDataSyncStoreService.resolveResourceContent(
this.resource,
lastSyncUserDataState.ref,
this.collection,
this.syncHeaders,
);
syncData = content === null ? null : this.parseSyncData(content);
await this.writeLastSyncStoredRemoteUserData({ ref: lastSyncUserDataState.ref, syncData });
} catch (error) {
if (error instanceof UserDataSyncError && error.code === 'NotFound' /* UserDataSyncErrorCode.NotFound */) {
this.logService.info(`${this.syncResourceLogLabel}: Last sync resource does not exist remotely.`);
} else {
throw error;
}
}
}
// Last Sync Data Not Found
if (syncData === undefined) {
return null;
}
return {
...lastSyncUserDataState,
syncData,
};
}
async updateLastSyncUserData(lastSyncRemoteUserData, additionalProps = {}) {
if (additionalProps['ref'] || additionalProps['version']) {
throw new Error('Cannot have core properties as additional');
}
const version = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
const lastSyncUserDataState = {
ref: lastSyncRemoteUserData.ref,
version,
...additionalProps,
};
this.storageService.store(
this.lastSyncUserDataStateKey,
JSON.stringify(lastSyncUserDataState),
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
await this.writeLastSyncStoredRemoteUserData(lastSyncRemoteUserData);
}
getStoredLastSyncUserDataStateContent() {
return this.storageService.get(this.lastSyncUserDataStateKey, -1 /* StorageScope.APPLICATION */);
}
async readLastSyncStoredRemoteUserData() {
const content = (await this.fileService.readFile(this.lastSyncResource)).value.toString();
try {
const lastSyncStoredRemoteUserData = content ? JSON.parse(content) : undefined;
if (isRemoteUserData(lastSyncStoredRemoteUserData)) {
return lastSyncStoredRemoteUserData;
}
} catch (e) {
this.logService.error(e);
}
return undefined;
}
async writeLastSyncStoredRemoteUserData(lastSyncRemoteUserData) {
await this.fileService.writeFile(
this.lastSyncResource,
VSBuffer.fromString(JSON.stringify(lastSyncRemoteUserData)),
);
}
async migrateLastSyncUserData() {
try {
const content = await this.fileService.readFile(this.lastSyncResource);
const userData = JSON.parse(content.value.toString());
await this.fileService.del(this.lastSyncResource);
if (userData.ref && userData.content !== undefined) {
this.storageService.store(
this.lastSyncUserDataStateKey,
JSON.stringify({
...userData,
content: undefined,
}),
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
await this.writeLastSyncStoredRemoteUserData({
ref: userData.ref,
syncData: userData.content === null ? null : JSON.parse(userData.content),
});
}
} catch (error) {
if (
error instanceof FileOperationError &&
error.fileOperationResult === 1 /* FileOperationResult.FILE_NOT_FOUND */
) {
this.logService.debug(`${this.syncResourceLogLabel}: Migrating last sync user data. Resource does not exist.`);
} else {
this.logService.error(error);
}
}
return this.storageService.get(this.lastSyncUserDataStateKey, -1 /* StorageScope.APPLICATION */);
}
async getRemoteUserData(lastSyncData) {
const { ref, content } = await this.getUserData(lastSyncData);
let syncData = null;
if (content !== null) {
syncData = this.parseSyncData(content);
}
return { ref, syncData };
}
parseSyncData(content) {
try {
const syncData = JSON.parse(content);
if (isSyncData(syncData)) {
return syncData;
}
} catch (error) {
this.logService.error(error);
}
throw new UserDataSyncError(
localize('incompatible sync data', 'Cannot parse sync data as it is not compatible with the current version.'),
'IncompatibleRemoteContent' /* UserDataSyncErrorCode.IncompatibleRemoteContent */,
this.resource,
);
}
async getUserData(lastSyncData) {
const lastSyncUserData = lastSyncData
? { ref: lastSyncData.ref, content: lastSyncData.syncData ? JSON.stringify(lastSyncData.syncData) : null }
: null;
return this.userDataSyncStoreService.readResource(
this.resource,
lastSyncUserData,
this.collection,
this.syncHeaders,
);
}
async updateRemoteUserData(content, ref) {
const machineId = await this.currentMachineIdPromise;
const syncData = { version: this.version, machineId, content };
try {
ref = await this.userDataSyncStoreService.writeResource(
this.resource,
JSON.stringify(syncData),
ref,
this.collection,
this.syncHeaders,
);
return { ref, syncData };
} catch (error) {
if (error instanceof UserDataSyncError && error.code === 'TooLarge' /* UserDataSyncErrorCode.TooLarge */) {
error = new UserDataSyncError(error.message, error.code, this.resource);
}
throw error;
}
}
async backupLocal(content) {
const syncData = { version: this.version, content };
return this.userDataSyncBackupStoreService.backup(
this.syncResource.profile,
this.resource,
JSON.stringify(syncData),
);
}
async stop() {
if (this.status === 'idle' /* 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('idle' /* SyncStatus.Idle */);
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
}
getUserDataSyncConfiguration() {
return this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE);
}
};
AbstractSynchroniser = __decorate(
[
__param(2, IFileService),
__param(3, IEnvironmentService),
__param(4, IStorageService),
__param(5, IUserDataSyncStoreService),
__param(6, IUserDataSyncBackupStoreService),
__param(7, IUserDataSyncEnablementService),
__param(8, ITelemetryService),
__param(9, IUserDataSyncLogService),
__param(10, IConfigurationService),
__param(11, IUriIdentityService),
],
AbstractSynchroniser,
);
export { AbstractSynchroniser };
let AbstractFileSynchroniser = class AbstractFileSynchroniser extends AbstractSynchroniser {
file;
constructor(
file,
syncResource,
collection,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
userDataSyncEnablementService,
telemetryService,
logService,
configurationService,
uriIdentityService,
) {
super(
syncResource,
collection,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
userDataSyncEnablementService,
telemetryService,
logService,
configurationService,
uriIdentityService,
);
this.file = file;
this._register(this.fileService.watch(this.extUri.dirname(file)));
this._register(this.fileService.onDidFilesChange((e) => this.onFileChanges(e)));
}
async getLocalFileContent() {
try {
return await this.fileService.readFile(this.file);
} catch (error) {
return null;
}
}
async updateLocalFileContent(newContent, oldContent, force) {
try {
if (oldContent) {
// file exists already
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: force });
}
} catch (e) {
if (
(e instanceof FileOperationError && e.fileOperationResult === 1) /* FileOperationResult.FILE_NOT_FOUND */ ||
(e instanceof FileOperationError && e.fileOperationResult === 3) /* FileOperationResult.FILE_MODIFIED_SINCE */
) {
throw new UserDataSyncError(
e.message,
'LocalPreconditionFailed' /* UserDataSyncErrorCode.LocalPreconditionFailed */,
);
} else {
throw e;
}
}
}
async deleteLocalFile() {
try {
await this.fileService.del(this.file);
} catch (e) {
if (
!((e instanceof FileOperationError && e.fileOperationResult === 1) /* FileOperationResult.FILE_NOT_FOUND */)
) {
throw e;
}
}
}
onFileChanges(e) {
if (!e.contains(this.file)) {
return;
}
this.triggerLocalChange();
}
};
AbstractFileSynchroniser = __decorate(
[
__param(3, IFileService),
__param(4, IEnvironmentService),
__param(5, IStorageService),
__param(6, IUserDataSyncStoreService),
__param(7, IUserDataSyncBackupStoreService),
__param(8, IUserDataSyncEnablementService),
__param(9, ITelemetryService),
__param(10, IUserDataSyncLogService),
__param(11, IConfigurationService),
__param(12, IUriIdentityService),
],
AbstractFileSynchroniser,
);
export { AbstractFileSynchroniser };
let AbstractJsonFileSynchroniser = class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
userDataSyncUtilService;
constructor(
file,
syncResource,
collection,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
userDataSyncEnablementService,
telemetryService,
logService,
userDataSyncUtilService,
configurationService,
uriIdentityService,
) {
super(
file,
syncResource,
collection,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
userDataSyncEnablementService,
telemetryService,
logService,
configurationService,
uriIdentityService,
);
this.userDataSyncUtilService = userDataSyncUtilService;
}
hasErrors(content, isArray) {
const parseErrors = [];
const result = parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
return parseErrors.length > 0 || (!isUndefined(result) && isArray !== Array.isArray(result));
}
_formattingOptions = undefined;
getFormattingOptions() {
if (!this._formattingOptions) {
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.file);
}
return this._formattingOptions;
}
};
AbstractJsonFileSynchroniser = __decorate(
[
__param(3, IFileService),
__param(4, IEnvironmentService),
__param(5, IStorageService),
__param(6, IUserDataSyncStoreService),
__param(7, IUserDataSyncBackupStoreService),
__param(8, IUserDataSyncEnablementService),
__param(9, ITelemetryService),
__param(10, IUserDataSyncLogService),
__param(11, IUserDataSyncUtilService),
__param(12, IConfigurationService),
__param(13, IUriIdentityService),
],
AbstractJsonFileSynchroniser,
);
export { AbstractJsonFileSynchroniser };
let AbstractInitializer = class AbstractInitializer {
resource;
userDataProfilesService;
environmentService;
logService;
fileService;
storageService;
extUri;
lastSyncResource;
constructor(
resource,
userDataProfilesService,
environmentService,
logService,
fileService,
storageService,
uriIdentityService,
) {
this.resource = resource;
this.userDataProfilesService = userDataProfilesService;
this.environmentService = environmentService;
this.logService = logService;
this.fileService = fileService;
this.storageService = storageService;
this.extUri = uriIdentityService.extUri;
this.lastSyncResource = getLastSyncResourceUri(undefined, this.resource, environmentService, this.extUri);
}
async initialize({ ref, content }) {
if (!content) {
this.logService.info('Remote content does not exist.', this.resource);
return;
}
const syncData = this.parseSyncData(content);
if (!syncData) {
return;
}
try {
await this.doInitialize({ ref, syncData });
} catch (error) {
this.logService.error(error);
}
}
parseSyncData(content) {
try {
const syncData = JSON.parse(content);
if (isSyncData(syncData)) {
return syncData;
}
} catch (error) {
this.logService.error(error);
}
this.logService.info('Cannot parse sync data as it is not compatible with the current version.', this.resource);
return undefined;
}
async updateLastSyncUserData(lastSyncRemoteUserData, additionalProps = {}) {
if (additionalProps['ref'] || additionalProps['version']) {
throw new Error('Cannot have core properties as additional');
}
const lastSyncUserDataState = {
ref: lastSyncRemoteUserData.ref,
version: undefined,
...additionalProps,
};
this.storageService.store(
`${this.resource}.lastSyncUserData`,
JSON.stringify(lastSyncUserDataState),
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
await this.fileService.writeFile(
this.lastSyncResource,
VSBuffer.fromString(JSON.stringify(lastSyncRemoteUserData)),
);
}
};
AbstractInitializer = __decorate(
[
__param(1, IUserDataProfilesService),
__param(2, IEnvironmentService),
__param(3, ILogService),
__param(4, IFileService),
__param(5, IStorageService),
__param(6, IUriIdentityService),
],
AbstractInitializer,
);
export { AbstractInitializer };