@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
677 lines (676 loc) • 25 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 { createCancelablePromise, disposableTimeout, ThrottledDelayer, timeout } from '@sussudio/base/common/async.mjs';
import { toLocalISOString } from '@sussudio/base/common/date.mjs';
import { toErrorMessage } from '@sussudio/base/common/errorMessage.mjs';
import { isCancellationError } from '@sussudio/base/common/errors.mjs';
import { Emitter, Event } from '@sussudio/base/common/event.mjs';
import { Disposable, MutableDisposable, toDisposable } from '@sussudio/base/common/lifecycle.mjs';
import { isWeb } from '@sussudio/base/common/platform.mjs';
import { isEqual } from '@sussudio/base/common/resources.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { localize } from 'vscode-nls.mjs';
import { IProductService } from '../../product/common/productService.mjs';
import { IStorageService } from '../../storage/common/storage.mjs';
import { ITelemetryService } from '../../telemetry/common/telemetry.mjs';
import {
IUserDataSyncLogService,
IUserDataSyncEnablementService,
IUserDataSyncService,
IUserDataSyncStoreManagementService,
IUserDataSyncStoreService,
UserDataAutoSyncError,
UserDataSyncError,
} from './userDataSync.mjs';
import { IUserDataSyncAccountService } from './userDataSyncAccount.mjs';
import { IUserDataSyncMachinesService } from './userDataSyncMachines.mjs';
const disableMachineEventuallyKey = 'sync.disableMachineEventually';
const sessionIdKey = 'sync.sessionId';
const storeUrlKey = 'sync.storeUrl';
const productQualityKey = 'sync.productQuality';
let UserDataAutoSyncService = class UserDataAutoSyncService extends Disposable {
userDataSyncStoreManagementService;
userDataSyncStoreService;
userDataSyncEnablementService;
userDataSyncService;
logService;
userDataSyncAccountService;
telemetryService;
userDataSyncMachinesService;
storageService;
_serviceBrand;
autoSync = this._register(new MutableDisposable());
successiveFailures = 0;
lastSyncTriggerTime = undefined;
syncTriggerDelayer;
suspendUntilRestart = false;
_onError = this._register(new Emitter());
onError = this._onError.event;
lastSyncUrl;
get syncUrl() {
const value = this.storageService.get(storeUrlKey, -1 /* StorageScope.APPLICATION */);
return value ? URI.parse(value) : undefined;
}
set syncUrl(syncUrl) {
if (syncUrl) {
this.storageService.store(
storeUrlKey,
syncUrl.toString(),
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
} else {
this.storageService.remove(storeUrlKey, -1 /* StorageScope.APPLICATION */);
}
}
previousProductQuality;
get productQuality() {
return this.storageService.get(productQualityKey, -1 /* StorageScope.APPLICATION */);
}
set productQuality(productQuality) {
if (productQuality) {
this.storageService.store(
productQualityKey,
productQuality,
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
} else {
this.storageService.remove(productQualityKey, -1 /* StorageScope.APPLICATION */);
}
}
constructor(
productService,
userDataSyncStoreManagementService,
userDataSyncStoreService,
userDataSyncEnablementService,
userDataSyncService,
logService,
userDataSyncAccountService,
telemetryService,
userDataSyncMachinesService,
storageService,
) {
super();
this.userDataSyncStoreManagementService = userDataSyncStoreManagementService;
this.userDataSyncStoreService = userDataSyncStoreService;
this.userDataSyncEnablementService = userDataSyncEnablementService;
this.userDataSyncService = userDataSyncService;
this.logService = logService;
this.userDataSyncAccountService = userDataSyncAccountService;
this.telemetryService = telemetryService;
this.userDataSyncMachinesService = userDataSyncMachinesService;
this.storageService = storageService;
this.syncTriggerDelayer = this._register(new ThrottledDelayer(this.getSyncTriggerDelayTime()));
this.lastSyncUrl = this.syncUrl;
this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url;
this.previousProductQuality = this.productQuality;
this.productQuality = productService.quality;
if (this.syncUrl) {
this.logService.info('Using settings sync service', this.syncUrl.toString());
this._register(
userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => {
if (!isEqual(this.syncUrl, userDataSyncStoreManagementService.userDataSyncStore?.url)) {
this.lastSyncUrl = this.syncUrl;
this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url;
if (this.syncUrl) {
this.logService.info('Using settings sync service', this.syncUrl.toString());
}
}
}),
);
if (this.userDataSyncEnablementService.isEnabled()) {
this.logService.info('Auto Sync is enabled.');
} else {
this.logService.info('Auto Sync is disabled.');
}
this.updateAutoSync();
if (this.hasToDisableMachineEventually()) {
this.disableMachineEventually();
}
this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync()));
this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync()));
this._register(userDataSyncService.onDidChangeLocal((source) => this.triggerSync([source], false, false)));
this._register(
Event.filter(
this.userDataSyncEnablementService.onDidChangeResourceEnablement,
([, enabled]) => enabled,
)(() => this.triggerSync(['resourceEnablement'], false, false)),
);
this._register(
this.userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() =>
this.triggerSync(['userDataSyncStoreChanged'], false, false),
),
);
}
}
updateAutoSync() {
const { enabled, message } = this.isAutoSyncEnabled();
if (enabled) {
if (this.autoSync.value === undefined) {
this.autoSync.value = new AutoSync(
this.lastSyncUrl,
1000 * 60 * 5 /* 5 miutes */,
this.userDataSyncStoreManagementService,
this.userDataSyncStoreService,
this.userDataSyncService,
this.userDataSyncMachinesService,
this.logService,
this.storageService,
);
this.autoSync.value.register(
this.autoSync.value.onDidStartSync(() => (this.lastSyncTriggerTime = new Date().getTime())),
);
this.autoSync.value.register(this.autoSync.value.onDidFinishSync((e) => this.onDidFinishSync(e)));
if (this.startAutoSync()) {
this.autoSync.value.start();
}
}
} else {
this.syncTriggerDelayer.cancel();
if (this.autoSync.value !== undefined) {
if (message) {
this.logService.info(message);
}
this.autoSync.clear();
} else if (message && this.userDataSyncEnablementService.isEnabled()) {
/* log message when auto sync is not disabled by user */
this.logService.info(message);
}
}
}
// For tests purpose only
startAutoSync() {
return true;
}
isAutoSyncEnabled() {
if (!this.userDataSyncEnablementService.isEnabled()) {
return { enabled: false, message: 'Auto Sync: Disabled.' };
}
if (!this.userDataSyncAccountService.account) {
return { enabled: false, message: 'Auto Sync: Suspended until auth token is available.' };
}
if (this.userDataSyncStoreService.donotMakeRequestsUntil) {
return {
enabled: false,
message: `Auto Sync: Suspended until ${toLocalISOString(
this.userDataSyncStoreService.donotMakeRequestsUntil,
)} because server is not accepting requests until then.`,
};
}
if (this.suspendUntilRestart) {
return { enabled: false, message: 'Auto Sync: Suspended until restart.' };
}
return { enabled: true };
}
async turnOn() {
this.stopDisableMachineEventually();
this.lastSyncUrl = this.syncUrl;
this.updateEnablement(true);
}
async turnOff(everywhere, softTurnOffOnError, donotRemoveMachine) {
try {
// Remove machine
if (this.userDataSyncAccountService.account && !donotRemoveMachine) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
// Disable Auto Sync
this.updateEnablement(false);
// Reset Session
this.storageService.remove(sessionIdKey, -1 /* StorageScope.APPLICATION */);
// Reset
if (everywhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
} catch (error) {
this.logService.error(error);
if (softTurnOffOnError) {
this.updateEnablement(false);
} else {
throw error;
}
}
}
updateEnablement(enabled) {
if (this.userDataSyncEnablementService.isEnabled() !== enabled) {
this.userDataSyncEnablementService.setEnablement(enabled);
this.updateAutoSync();
}
}
hasProductQualityChanged() {
return (
!!this.previousProductQuality && !!this.productQuality && this.previousProductQuality !== this.productQuality
);
}
async onDidFinishSync(error) {
if (!error) {
// Sync finished without errors
this.successiveFailures = 0;
return;
}
// Error while syncing
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
// Log to telemetry
if (userDataSyncError instanceof UserDataAutoSyncError) {
this.telemetryService.publicLog2(`autosync/error`, {
code: userDataSyncError.code,
service: this.userDataSyncStoreManagementService.userDataSyncStore.url.toString(),
});
}
// Session got expired
if (userDataSyncError.code === 'SessionExpired' /* UserDataSyncErrorCode.SessionExpired */) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because current session is expired');
}
// Turned off from another device
else if (userDataSyncError.code === 'TurnedOff' /* UserDataSyncErrorCode.TurnedOff */) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
}
// Exceeded Rate Limit on Client
else if (userDataSyncError.code === 'LocalTooManyRequests' /* UserDataSyncErrorCode.LocalTooManyRequests */) {
this.suspendUntilRestart = true;
this.logService.info('Auto Sync: Suspended sync because of making too many requests to server');
this.updateAutoSync();
}
// Exceeded Rate Limit on Server
else if (userDataSyncError.code === 'RemoteTooManyRequests' /* UserDataSyncErrorCode.TooManyRequests */) {
await this.turnOff(
false,
true /* force soft turnoff on error */,
true /* do not disable machine because disabling a machine makes request to server and can fail with TooManyRequests */,
);
this.disableMachineEventually();
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
}
// Method Not Found
else if (userDataSyncError.code === 'MethodNotFound' /* UserDataSyncErrorCode.MethodNotFound */) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info(
'Auto Sync: Turned off sync because current client is making requests to server that are not supported',
);
}
// Upgrade Required or Gone
else if (
userDataSyncError.code === 'UpgradeRequired' /* UserDataSyncErrorCode.UpgradeRequired */ ||
userDataSyncError.code === 'Gone' /* UserDataSyncErrorCode.Gone */
) {
await this.turnOff(
false,
true /* force soft turnoff on error */,
true /* do not disable machine because disabling a machine makes request to server and can fail with upgrade required or gone */,
);
this.disableMachineEventually();
this.logService.info(
'Auto Sync: Turned off sync because current client is not compatible with server. Requires client upgrade.',
);
}
// Incompatible Local Content
else if (
userDataSyncError.code === 'IncompatibleLocalContent' /* UserDataSyncErrorCode.IncompatibleLocalContent */
) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info(
`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`,
);
}
// Incompatible Remote Content
else if (
userDataSyncError.code === 'IncompatibleRemoteContent' /* UserDataSyncErrorCode.IncompatibleRemoteContent */
) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info(
`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`,
);
}
// Service changed
else if (
userDataSyncError.code === 'ServiceChanged' /* UserDataSyncErrorCode.ServiceChanged */ ||
userDataSyncError.code === 'DefaultServiceChanged' /* UserDataSyncErrorCode.DefaultServiceChanged */
) {
// Check if default settings sync service has changed in web without changing the product quality
// Then turn off settings sync and ask user to turn on again
if (
isWeb &&
userDataSyncError.code === 'DefaultServiceChanged' /* UserDataSyncErrorCode.DefaultServiceChanged */ &&
!this.hasProductQualityChanged()
) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because default sync service is changed.');
}
// Service has changed by the user. So turn off and turn on sync.
// Show a prompt to the user about service change.
else {
await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */);
await this.turnOn();
this.logService.info(
'Auto Sync: Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.',
);
}
} else {
this.logService.error(userDataSyncError);
this.successiveFailures++;
}
this._onError.fire(userDataSyncError);
}
async disableMachineEventually() {
this.storageService.store(
disableMachineEventuallyKey,
true,
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
await timeout(1000 * 60 * 10);
// Return if got stopped meanwhile.
if (!this.hasToDisableMachineEventually()) {
return;
}
this.stopDisableMachineEventually();
// disable only if sync is disabled
if (!this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAccountService.account) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
}
hasToDisableMachineEventually() {
return this.storageService.getBoolean(disableMachineEventuallyKey, -1 /* StorageScope.APPLICATION */, false);
}
stopDisableMachineEventually() {
this.storageService.remove(disableMachineEventuallyKey, -1 /* StorageScope.APPLICATION */);
}
sources = [];
async triggerSync(sources, skipIfSyncedRecently, disableCache) {
if (this.autoSync.value === undefined) {
return this.syncTriggerDelayer.cancel();
}
if (
skipIfSyncedRecently &&
this.lastSyncTriggerTime &&
Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10
) {
this.logService.debug('Auto Sync: Skipped. Limited to once per 10 seconds.');
return;
}
this.sources.push(...sources);
return this.syncTriggerDelayer.trigger(
async () => {
this.logService.trace('activity sources', ...this.sources);
const providerId = this.userDataSyncAccountService.account?.authenticationProviderId || '';
this.telemetryService.publicLog2('sync/triggered', { sources: this.sources, providerId });
this.sources = [];
if (this.autoSync.value) {
await this.autoSync.value.sync('Activity', disableCache);
}
},
this.successiveFailures
? this.getSyncTriggerDelayTime() *
1 *
Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
: this.getSyncTriggerDelayTime(),
);
}
getSyncTriggerDelayTime() {
return 2000; /* Debounce for 2 seconds if there are no failures */
}
};
UserDataAutoSyncService = __decorate(
[
__param(0, IProductService),
__param(1, IUserDataSyncStoreManagementService),
__param(2, IUserDataSyncStoreService),
__param(3, IUserDataSyncEnablementService),
__param(4, IUserDataSyncService),
__param(5, IUserDataSyncLogService),
__param(6, IUserDataSyncAccountService),
__param(7, ITelemetryService),
__param(8, IUserDataSyncMachinesService),
__param(9, IStorageService),
],
UserDataAutoSyncService,
);
export { UserDataAutoSyncService };
class AutoSync extends Disposable {
lastSyncUrl;
interval;
userDataSyncStoreManagementService;
userDataSyncStoreService;
userDataSyncService;
userDataSyncMachinesService;
logService;
storageService;
static INTERVAL_SYNCING = 'Interval';
intervalHandler = this._register(new MutableDisposable());
_onDidStartSync = this._register(new Emitter());
onDidStartSync = this._onDidStartSync.event;
_onDidFinishSync = this._register(new Emitter());
onDidFinishSync = this._onDidFinishSync.event;
manifest = null;
syncTask;
syncPromise;
constructor(
lastSyncUrl,
interval /* in milliseconds */,
userDataSyncStoreManagementService,
userDataSyncStoreService,
userDataSyncService,
userDataSyncMachinesService,
logService,
storageService,
) {
super();
this.lastSyncUrl = lastSyncUrl;
this.interval = interval;
this.userDataSyncStoreManagementService = userDataSyncStoreManagementService;
this.userDataSyncStoreService = userDataSyncStoreService;
this.userDataSyncService = userDataSyncService;
this.userDataSyncMachinesService = userDataSyncMachinesService;
this.logService = logService;
this.storageService = storageService;
}
start() {
this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync()));
this._register(
toDisposable(() => {
if (this.syncPromise) {
this.syncPromise.cancel();
this.logService.info('Auto sync: Cancelled sync that is in progress');
this.syncPromise = undefined;
}
this.syncTask?.stop();
this.logService.info('Auto Sync: Stopped');
}),
);
this.sync(AutoSync.INTERVAL_SYNCING, false);
}
waitUntilNextIntervalAndSync() {
this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING, false), this.interval);
}
sync(reason, disableCache) {
const syncPromise = createCancelablePromise(async (token) => {
if (this.syncPromise) {
try {
// Wait until existing sync is finished
this.logService.debug('Auto Sync: Waiting until sync is finished.');
await this.syncPromise;
} catch (error) {
if (isCancellationError(error)) {
// Cancelled => Disposed. Donot continue sync.
return;
}
}
}
return this.doSync(reason, disableCache, token);
});
this.syncPromise = syncPromise;
this.syncPromise.finally(() => (this.syncPromise = undefined));
return this.syncPromise;
}
hasSyncServiceChanged() {
return (
this.lastSyncUrl !== undefined &&
!isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url)
);
}
async hasDefaultServiceChanged() {
const previous = await this.userDataSyncStoreManagementService.getPreviousUserDataSyncStore();
const current = this.userDataSyncStoreManagementService.userDataSyncStore;
// check if defaults changed
return (
!!current &&
!!previous &&
(!isEqual(current.defaultUrl, previous.defaultUrl) ||
!isEqual(current.insidersUrl, previous.insidersUrl) ||
!isEqual(current.stableUrl, previous.stableUrl))
);
}
async doSync(reason, disableCache, token) {
this.logService.info(`Auto Sync: Triggered by ${reason}`);
this._onDidStartSync.fire();
let error;
try {
await this.createAndRunSyncTask(disableCache, token);
} catch (e) {
this.logService.error(e);
error = e;
if (
UserDataSyncError.toUserDataSyncError(e).code === 'MethodNotFound' /* UserDataSyncErrorCode.MethodNotFound */
) {
try {
this.logService.info('Auto Sync: Client is making invalid requests. Cleaning up data...');
await this.userDataSyncService.cleanUpRemoteData();
this.logService.info('Auto Sync: Retrying sync...');
await this.createAndRunSyncTask(disableCache, token);
error = undefined;
} catch (e1) {
this.logService.error(e1);
error = e1;
}
}
}
this._onDidFinishSync.fire(error);
}
async createAndRunSyncTask(disableCache, token) {
this.syncTask = await this.userDataSyncService.createSyncTask(this.manifest, disableCache);
if (token.isCancellationRequested) {
return;
}
this.manifest = this.syncTask.manifest;
// Server has no data but this machine was synced before
if (this.manifest === null && (await this.userDataSyncService.hasPreviouslySynced())) {
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(
localize('default service changed', 'Cannot sync because default service has changed'),
'DefaultServiceChanged' /* UserDataSyncErrorCode.DefaultServiceChanged */,
);
} else {
throw new UserDataAutoSyncError(
localize('service changed', 'Cannot sync because sync service has changed'),
'ServiceChanged' /* UserDataSyncErrorCode.ServiceChanged */,
);
}
} else {
// Sync was turned off in the cloud
throw new UserDataAutoSyncError(
localize('turned off', 'Cannot sync because syncing is turned off in the cloud'),
'TurnedOff' /* UserDataSyncErrorCode.TurnedOff */,
);
}
}
const sessionId = this.storageService.get(sessionIdKey, -1 /* StorageScope.APPLICATION */);
// Server session is different from client session
if (sessionId && this.manifest && sessionId !== this.manifest.session) {
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(
localize('default service changed', 'Cannot sync because default service has changed'),
'DefaultServiceChanged' /* UserDataSyncErrorCode.DefaultServiceChanged */,
);
} else {
throw new UserDataAutoSyncError(
localize('service changed', 'Cannot sync because sync service has changed'),
'ServiceChanged' /* UserDataSyncErrorCode.ServiceChanged */,
);
}
} else {
throw new UserDataAutoSyncError(
localize('session expired', 'Cannot sync because current session is expired'),
'SessionExpired' /* UserDataSyncErrorCode.SessionExpired */,
);
}
}
const machines = await this.userDataSyncMachinesService.getMachines(this.manifest || undefined);
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
const currentMachine = machines.find((machine) => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Throw TurnedOff error
throw new UserDataAutoSyncError(
localize(
'turned off machine',
'Cannot sync because syncing is turned off on this machine from another machine.',
),
'TurnedOff' /* UserDataSyncErrorCode.TurnedOff */,
);
}
await this.syncTask.run();
// After syncing, get the manifest if it was not available before
if (this.manifest === null) {
try {
this.manifest = await this.userDataSyncStoreService.manifest(null);
} catch (error) {
throw new UserDataAutoSyncError(
toErrorMessage(error),
error instanceof UserDataSyncError ? error.code : 'Unknown' /* UserDataSyncErrorCode.Unknown */,
);
}
}
// Update local session id
if (this.manifest && this.manifest.session !== sessionId) {
this.storageService.store(
sessionIdKey,
this.manifest.session,
-1 /* StorageScope.APPLICATION */,
1 /* StorageTarget.MACHINE */,
);
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
// Add current machine
if (!currentMachine) {
await this.userDataSyncMachinesService.addCurrentMachine(this.manifest || undefined);
}
}
register(t) {
return super._register(t);
}
}