@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
377 lines (376 loc) • 15.3 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 { toFormattedString } from '@sussudio/base/common/jsonFormatter.mjs';
import { IConfigurationService } from '../../configuration/common/configuration.mjs';
import { IEnvironmentService } from '../../environment/common/environment.mjs';
import { IFileService } from '../../files/common/files.mjs';
import { IStorageService } from '../../storage/common/storage.mjs';
import { ITelemetryService } from '../../telemetry/common/telemetry.mjs';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.mjs';
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.mjs';
import { AbstractSynchroniser } from './abstractSynchronizer.mjs';
import { merge } from './userDataProfilesManifestMerge.mjs';
import {
IUserDataSyncBackupStoreService,
IUserDataSyncLogService,
IUserDataSyncEnablementService,
IUserDataSyncStoreService,
USER_DATA_SYNC_SCHEME,
UserDataSyncError,
} from './userDataSync.mjs';
let UserDataProfilesManifestSynchroniser = class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser {
userDataProfilesService;
version = 1;
previewResource = this.extUri.joinPath(this.syncPreviewFolder, 'profiles.json');
baseResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
localResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
remoteResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
acceptedResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
constructor(
profile,
collection,
userDataProfilesService,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
logService,
configurationService,
userDataSyncEnablementService,
telemetryService,
uriIdentityService,
) {
super(
{ syncResource: 'profiles' /* SyncResource.Profiles */, profile },
collection,
fileService,
environmentService,
storageService,
userDataSyncStoreService,
userDataSyncBackupStoreService,
userDataSyncEnablementService,
telemetryService,
logService,
configurationService,
uriIdentityService,
);
this.userDataProfilesService = userDataProfilesService;
}
async getLastSyncedProfiles() {
const lastSyncUserData = await this.getLastSyncUserData();
return lastSyncUserData?.syncData ? parseUserDataProfilesManifest(lastSyncUserData.syncData) : null;
}
async getRemoteSyncedProfiles(manifest) {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
return remoteUserData?.syncData ? parseUserDataProfilesManifest(remoteUserData.syncData) : null;
}
async generateSyncPreview(remoteUserData, lastSyncUserData, isRemoteDataFromCurrentMachine) {
if (!this.userDataProfilesService.isEnabled()) {
throw new UserDataSyncError(
'Cannot sync profiles because they are disabled',
'LocalError' /* UserDataSyncErrorCode.LocalError */,
);
}
const remoteProfiles = remoteUserData.syncData ? parseUserDataProfilesManifest(remoteUserData.syncData) : null;
const lastSyncProfiles = lastSyncUserData?.syncData
? parseUserDataProfilesManifest(lastSyncUserData.syncData)
: null;
const localProfiles = this.getLocalUserDataProfiles();
const { local, remote } = merge(localProfiles, remoteProfiles, lastSyncProfiles, []);
const previewResult = {
local,
remote,
content: lastSyncProfiles ? this.stringifyRemoteProfiles(lastSyncProfiles) : null,
localChange:
local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0
? 2 /* Change.Modified */
: 0 /* Change.None */,
remoteChange: remote !== null ? 2 /* Change.Modified */ : 0 /* Change.None */,
};
const localContent = stringifyLocalProfiles(localProfiles, false);
return [
{
baseResource: this.baseResource,
baseContent: lastSyncProfiles ? this.stringifyRemoteProfiles(lastSyncProfiles) : null,
localResource: this.localResource,
localContent,
remoteResource: this.remoteResource,
remoteContent: remoteProfiles ? this.stringifyRemoteProfiles(remoteProfiles) : null,
remoteProfiles,
previewResource: this.previewResource,
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: this.acceptedResource,
},
];
}
async hasRemoteChanged(lastSyncUserData) {
const lastSyncProfiles = lastSyncUserData?.syncData
? parseUserDataProfilesManifest(lastSyncUserData.syncData)
: null;
const localProfiles = this.getLocalUserDataProfiles();
const { remote } = merge(localProfiles, lastSyncProfiles, lastSyncProfiles, []);
return !!remote?.added.length || !!remote?.removed.length || !!remote?.updated.length;
}
async getMergeResult(resourcePreview, token) {
return { ...resourcePreview.previewResult, hasConflicts: false };
}
async getAcceptResult(resourcePreview, resource, content, token) {
/* Accept local resource */
if (this.extUri.isEqual(resource, this.localResource)) {
return this.acceptLocal(resourcePreview);
}
/* Accept remote resource */
if (this.extUri.isEqual(resource, this.remoteResource)) {
return this.acceptRemote(resourcePreview);
}
/* Accept preview resource */
if (this.extUri.isEqual(resource, this.previewResource)) {
return resourcePreview.previewResult;
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
async acceptLocal(resourcePreview) {
const localProfiles = this.getLocalUserDataProfiles();
const mergeResult = merge(localProfiles, null, null, []);
const { local, remote } = mergeResult;
return {
content: resourcePreview.localContent,
local,
remote,
localChange:
local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0
? 2 /* Change.Modified */
: 0 /* Change.None */,
remoteChange: remote !== null ? 2 /* Change.Modified */ : 0 /* Change.None */,
};
}
async acceptRemote(resourcePreview) {
const remoteProfiles = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
const lastSyncProfiles = [];
const localProfiles = [];
for (const profile of this.getLocalUserDataProfiles()) {
const remoteProfile = remoteProfiles?.find((remoteProfile) => remoteProfile.id === profile.id);
if (remoteProfile) {
lastSyncProfiles.push({ id: profile.id, name: profile.name, collection: remoteProfile.collection });
localProfiles.push(profile);
}
}
if (remoteProfiles !== null) {
const mergeResult = merge(localProfiles, remoteProfiles, lastSyncProfiles, []);
const { local, remote } = mergeResult;
return {
content: resourcePreview.remoteContent,
local,
remote,
localChange:
local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0
? 2 /* Change.Modified */
: 0 /* Change.None */,
remoteChange: remote !== null ? 2 /* Change.Modified */ : 0 /* Change.None */,
};
} else {
return {
content: resourcePreview.remoteContent,
local: { added: [], removed: [], updated: [] },
remote: null,
localChange: 0 /* Change.None */,
remoteChange: 0 /* Change.None */,
};
}
}
async applyResult(remoteUserData, lastSyncUserData, resourcePreviews, force) {
const { local, remote, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === 0 /* Change.None */ && remoteChange === 0 /* Change.None */) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing profiles.`);
}
const remoteProfiles = resourcePreviews[0][0].remoteProfiles || [];
if (remoteProfiles.length + (remote?.added.length ?? 0) - (remote?.removed.length ?? 0) > 20) {
throw new UserDataSyncError(
'Too many profiles to sync. Please remove some profiles and try again.',
'LocalTooManyProfiles' /* UserDataSyncErrorCode.LocalTooManyProfiles */,
);
}
if (localChange !== 0 /* Change.None */) {
await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false));
const promises = [];
for (const profile of local.added) {
promises.push(
(async () => {
this.logService.trace(`${this.syncResourceLogLabel}: Creating '${profile.name}' profile...`);
await this.userDataProfilesService.createProfile(profile.id, profile.name, {
shortName: profile.shortName,
});
this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`);
})(),
);
}
for (const profile of local.removed) {
promises.push(
(async () => {
this.logService.trace(`${this.syncResourceLogLabel}: Removing '${profile.name}' profile...`);
await this.userDataProfilesService.removeProfile(profile);
this.logService.info(`${this.syncResourceLogLabel}: Removed profile '${profile.name}'.`);
})(),
);
}
for (const profile of local.updated) {
const localProfile = this.userDataProfilesService.profiles.find((p) => p.id === profile.id);
if (localProfile) {
promises.push(
(async () => {
this.logService.trace(`${this.syncResourceLogLabel}: Updating '${profile.name}' profile...`);
await this.userDataProfilesService.updateProfile(localProfile, {
name: profile.name,
shortName: profile.shortName,
});
this.logService.info(`${this.syncResourceLogLabel}: Updated profile '${profile.name}'.`);
})(),
);
} else {
this.logService.info(
`${this.syncResourceLogLabel}: Could not find profile with id '${profile.id}' to update.`,
);
}
}
await Promise.all(promises);
}
if (remoteChange !== 0 /* Change.None */) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote profiles...`);
const addedCollections = [];
const canAddRemoteProfiles = remoteProfiles.length + (remote?.added.length ?? 0) <= 20;
if (canAddRemoteProfiles) {
for (const profile of remote?.added || []) {
const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders);
addedCollections.push(collection);
remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName });
}
} else {
this.logService.info(
`${this.syncResourceLogLabel}: Could not create remote profiles as there are too many profiles.`,
);
}
for (const profile of remote?.removed || []) {
remoteProfiles.splice(
remoteProfiles.findIndex(({ id }) => profile.id === id),
1,
);
}
for (const profile of remote?.updated || []) {
const profileToBeUpdated = remoteProfiles.find(({ id }) => profile.id === id);
if (profileToBeUpdated) {
remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, {
id: profile.id,
name: profile.name,
collection: profileToBeUpdated.collection,
shortName: profile.shortName,
});
}
}
try {
remoteUserData = await this.updateRemoteProfiles(remoteProfiles, force ? null : remoteUserData.ref);
this.logService.info(
`${this.syncResourceLogLabel}: Updated remote profiles.${
canAddRemoteProfiles && remote?.added.length
? ` Added: ${JSON.stringify(remote.added.map((e) => e.name))}.`
: ''
}${remote?.updated.length ? ` Updated: ${JSON.stringify(remote.updated.map((e) => e.name))}.` : ''}${
remote?.removed.length ? ` Removed: ${JSON.stringify(remote.removed.map((e) => e.name))}.` : ''
}`,
);
} catch (error) {
if (addedCollections.length) {
this.logService.info(
`${this.syncResourceLogLabel}: Failed to update remote profiles. Cleaning up added collections...`,
);
for (const collection of addedCollections) {
await this.userDataSyncStoreService.deleteCollection(collection, this.syncHeaders);
}
}
throw error;
}
for (const profile of remote?.removed || []) {
await this.userDataSyncStoreService.deleteCollection(profile.collection, this.syncHeaders);
}
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized profiles...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized profiles.`);
}
}
async updateRemoteProfiles(profiles, ref) {
return this.updateRemoteUserData(this.stringifyRemoteProfiles(profiles), ref);
}
async hasLocalData() {
return this.getLocalUserDataProfiles().length > 0;
}
async resolveContent(uri) {
if (
this.extUri.isEqual(this.remoteResource, uri) ||
this.extUri.isEqual(this.baseResource, uri) ||
this.extUri.isEqual(this.localResource, uri) ||
this.extUri.isEqual(this.acceptedResource, uri)
) {
const content = await this.resolvePreviewContent(uri);
return content ? toFormattedString(JSON.parse(content), {}) : content;
}
return null;
}
getLocalUserDataProfiles() {
return this.userDataProfilesService.profiles.filter((p) => !p.isDefault && !p.isTransient);
}
stringifyRemoteProfiles(profiles) {
return JSON.stringify([...profiles].sort((a, b) => a.name.localeCompare(b.name)));
}
};
UserDataProfilesManifestSynchroniser = __decorate(
[
__param(2, IUserDataProfilesService),
__param(3, IFileService),
__param(4, IEnvironmentService),
__param(5, IStorageService),
__param(6, IUserDataSyncStoreService),
__param(7, IUserDataSyncBackupStoreService),
__param(8, IUserDataSyncLogService),
__param(9, IConfigurationService),
__param(10, IUserDataSyncEnablementService),
__param(11, ITelemetryService),
__param(12, IUriIdentityService),
],
UserDataProfilesManifestSynchroniser,
);
export { UserDataProfilesManifestSynchroniser };
export function stringifyLocalProfiles(profiles, format) {
const result = [...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ id: p.id, name: p.name }));
return format ? toFormattedString(result, {}) : JSON.stringify(result);
}
export function parseUserDataProfilesManifest(syncData) {
return JSON.parse(syncData.content);
}