mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-27 17:23:21 -05:00
Merge from vscode 8df646d3c5477b02737fc10343fa7cf0cc3f606b
This commit is contained in:
@@ -4,63 +4,120 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { IGlobalState } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export function merge(localGloablState: IGlobalState, remoteGlobalState: IGlobalState | null, lastSyncGlobalState: IGlobalState | null): { local?: IGlobalState, remote?: IGlobalState } {
|
||||
if (!remoteGlobalState) {
|
||||
return { remote: localGloablState };
|
||||
}
|
||||
|
||||
const { local: localArgv, remote: remoteArgv } = doMerge(localGloablState.argv, remoteGlobalState.argv, lastSyncGlobalState ? lastSyncGlobalState.argv : null);
|
||||
const { local: localStorage, remote: remoteStorage } = doMerge(localGloablState.storage, remoteGlobalState.storage, lastSyncGlobalState ? lastSyncGlobalState.storage : null);
|
||||
const local: IGlobalState | undefined = localArgv || localStorage ? { argv: localArgv || localGloablState.argv, storage: localStorage || localGloablState.storage } : undefined;
|
||||
const remote: IGlobalState | undefined = remoteArgv || remoteStorage ? { argv: remoteArgv || remoteGlobalState.argv, storage: remoteStorage || remoteGlobalState.storage } : undefined;
|
||||
|
||||
return { local, remote };
|
||||
export interface IMergeResult {
|
||||
local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
remote: IStringDictionary<IStorageValue> | null;
|
||||
}
|
||||
|
||||
function doMerge(local: IStringDictionary<any>, remote: IStringDictionary<any>, base: IStringDictionary<any> | null): { local?: IStringDictionary<any>, remote?: IStringDictionary<any> } {
|
||||
const localToRemote = compare(local, remote);
|
||||
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, logService: ILogService): IMergeResult {
|
||||
if (!remoteStorage) {
|
||||
return { remote: localStorage, local: { added: {}, removed: [], updated: {} } };
|
||||
}
|
||||
|
||||
const localToRemote = compare(localStorage, remoteStorage);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return {};
|
||||
return { remote: null, local: { added: {}, removed: [], updated: {} } };
|
||||
}
|
||||
|
||||
const baseToRemote = base ? compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
if (baseToRemote.added.size === 0 && baseToRemote.removed.size === 0 && baseToRemote.updated.size === 0) {
|
||||
// No changes found between base and remote.
|
||||
return { remote: local };
|
||||
}
|
||||
const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToLocal = baseStorage ? compare(baseStorage, localStorage) : { added: Object.keys(localStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
|
||||
const baseToLocal = base ? compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
if (baseToLocal.added.size === 0 && baseToLocal.removed.size === 0 && baseToLocal.updated.size === 0) {
|
||||
// No changes found between base and local.
|
||||
return { local: remote };
|
||||
}
|
||||
|
||||
const merged = objects.deepClone(local);
|
||||
const local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> } = { added: {}, removed: [], updated: {} };
|
||||
const remote: IStringDictionary<IStorageValue> = objects.deepClone(remoteStorage);
|
||||
|
||||
// Added in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
merged[key] = remote[key];
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
if (localValue && localValue.value === remoteValue.value) {
|
||||
continue;
|
||||
}
|
||||
if (baseToLocal.added.has(key)) {
|
||||
local.updated[key] = remoteValue;
|
||||
} else {
|
||||
local.added[key] = remoteValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Updated in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
merged[key] = remote[key];
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
if (localValue && localValue.value === remoteValue.value) {
|
||||
continue;
|
||||
}
|
||||
local.updated[key] = remoteValue;
|
||||
}
|
||||
|
||||
// Removed in remote & local
|
||||
// Removed in remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
// Got removed in local
|
||||
if (baseToLocal.removed.has(key)) {
|
||||
delete merged[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
local.removed.push(key);
|
||||
}
|
||||
|
||||
// Added in local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
if (!baseToRemote.added.has(key)) {
|
||||
remote[key] = localStorage[key];
|
||||
}
|
||||
}
|
||||
|
||||
return { local: merged, remote: merged };
|
||||
// Updated in local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
if (baseToRemote.updated.has(key) || baseToRemote.removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const remoteValue = remote[key];
|
||||
const localValue = localStorage[key];
|
||||
if (localValue.version < remoteValue.version) {
|
||||
continue;
|
||||
}
|
||||
remote[key] = localValue;
|
||||
}
|
||||
|
||||
// Removed in local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const remoteValue = remote[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (storageKey && storageKey.version < remoteValue.version) {
|
||||
continue;
|
||||
}
|
||||
delete remote[key];
|
||||
}
|
||||
|
||||
return { local, remote: areSame(remote, remoteStorage) ? null : remote };
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
@@ -83,3 +140,9 @@ function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { ad
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function areSame(a: IStringDictionary<IStorageValue>, b: IStringDictionary<IStorageValue>): boolean {
|
||||
const { added, removed, updated } = compare(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -19,12 +19,15 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly local: IGlobalState | undefined;
|
||||
readonly remote: IGlobalState | undefined;
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
@@ -43,10 +46,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
) {
|
||||
super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key))(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
@@ -65,9 +71,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const local: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
await this.apply({ local, remote: undefined, remoteUserData, localUserData, lastSyncUserData });
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
const { local, remote } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), this.logService);
|
||||
await this.apply({ local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData });
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -96,7 +103,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ local: undefined, remote: localUserData, remoteUserData, localUserData, lastSyncUserData }, true);
|
||||
await this.apply({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData }, true);
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`);
|
||||
} finally {
|
||||
@@ -136,8 +143,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
if (localGloablState.argv['locale'] !== 'en') {
|
||||
const { storage } = await this.getLocalGlobalState();
|
||||
if (Object.keys(storage).length > 1 || storage[`${argvStoragePrefx}.locale`]?.value !== 'en') {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -154,7 +161,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
|
||||
private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
|
||||
@@ -164,20 +171,21 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
|
||||
}
|
||||
|
||||
const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState);
|
||||
const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), this.logService);
|
||||
|
||||
return { local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData };
|
||||
}
|
||||
|
||||
private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasChanges = local || remote;
|
||||
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
|
||||
if (!hasChanges) {
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
}
|
||||
|
||||
if (local) {
|
||||
if (hasLocalChanged) {
|
||||
// update local
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
|
||||
await this.backupLocal(JSON.stringify(localUserData));
|
||||
@@ -185,10 +193,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
if (hasRemoteChanged) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
|
||||
const content = JSON.stringify(remote);
|
||||
const content = JSON.stringify(<IGlobalState>{ storage: remote });
|
||||
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
|
||||
}
|
||||
@@ -202,16 +210,21 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
}
|
||||
|
||||
private async getLocalGlobalState(): Promise<IGlobalState> {
|
||||
const argv: IStringDictionary<any> = {};
|
||||
const storage: IStringDictionary<any> = {};
|
||||
const storage: IStringDictionary<IStorageValue> = {};
|
||||
const argvContent: string = await this.getLocalArgvContent();
|
||||
const argvValue: IStringDictionary<any> = parse(argvContent);
|
||||
for (const argvProperty of argvProperties) {
|
||||
if (argvValue[argvProperty] !== undefined) {
|
||||
argv[argvProperty] = argvValue[argvProperty];
|
||||
storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] };
|
||||
}
|
||||
}
|
||||
return { argv, storage };
|
||||
for (const { key, version } of this.storageKeysSyncRegistryService.storageKeys) {
|
||||
const value = this.storageService.get(key, StorageScope.GLOBAL);
|
||||
if (value) {
|
||||
storage[key] = { version, value };
|
||||
}
|
||||
}
|
||||
return { storage };
|
||||
}
|
||||
|
||||
private async getLocalArgvContent(): Promise<string> {
|
||||
@@ -222,15 +235,59 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
return '{}';
|
||||
}
|
||||
|
||||
private async writeLocalGlobalState(globalState: IGlobalState): Promise<void> {
|
||||
const argvContent = await this.getLocalArgvContent();
|
||||
let content = argvContent;
|
||||
for (const argvProperty of Object.keys(globalState.argv)) {
|
||||
content = edit(content, [argvProperty], globalState.argv[argvProperty], {});
|
||||
private async writeLocalGlobalState({ added, removed, updated }: { added: IStringDictionary<IStorageValue>, updated: IStringDictionary<IStorageValue>, removed: string[] }): Promise<void> {
|
||||
const argv: IStringDictionary<any> = {};
|
||||
const updatedStorage: IStringDictionary<any> = {};
|
||||
const handleUpdatedStorage = (keys: string[], storage?: IStringDictionary<IStorageValue>): void => {
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(argvStoragePrefx)) {
|
||||
argv[key.substring(argvStoragePrefx.length)] = storage ? storage[key].value : undefined;
|
||||
continue;
|
||||
}
|
||||
if (storage) {
|
||||
const storageValue = storage[key];
|
||||
if (storageValue.value !== String(this.storageService.get(key, StorageScope.GLOBAL))) {
|
||||
updatedStorage[key] = storageValue.value;
|
||||
}
|
||||
} else {
|
||||
if (this.storageService.get(key, StorageScope.GLOBAL) !== undefined) {
|
||||
updatedStorage[key] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
handleUpdatedStorage(Object.keys(added), added);
|
||||
handleUpdatedStorage(Object.keys(updated), updated);
|
||||
handleUpdatedStorage(removed);
|
||||
if (Object.keys(argv).length) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`);
|
||||
await this.updateArgv(argv);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated locale`);
|
||||
}
|
||||
if (argvContent !== content) {
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content));
|
||||
const updatedStorageKeys: string[] = Object.keys(updatedStorage);
|
||||
if (updatedStorageKeys.length) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`);
|
||||
for (const key of Object.keys(updatedStorage)) {
|
||||
this.storageService.store(key, updatedStorage[key], StorageScope.GLOBAL);
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, Object.keys(updatedStorage));
|
||||
}
|
||||
}
|
||||
|
||||
private async updateArgv(argv: IStringDictionary<any>): Promise<void> {
|
||||
const argvContent = await this.getLocalArgvContent();
|
||||
let content = argvContent;
|
||||
for (const argvProperty of Object.keys(argv)) {
|
||||
content = edit(content, [argvProperty], argv[argvProperty], {});
|
||||
}
|
||||
if (argvContent !== content) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`);
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content));
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated locale.`);
|
||||
}
|
||||
}
|
||||
|
||||
private getSyncStorageKeys(): IStorageKey[] {
|
||||
return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => (<IStorageKey>{ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))];
|
||||
}
|
||||
}
|
||||
|
||||
63
src/vs/platform/userDataSync/common/storageKeys.ts
Normal file
63
src/vs/platform/userDataSync/common/storageKeys.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IStorageKey {
|
||||
|
||||
readonly key: string;
|
||||
readonly version: number;
|
||||
|
||||
}
|
||||
|
||||
export const IStorageKeysSyncRegistryService = createDecorator<IStorageKeysSyncRegistryService>('IStorageKeysSyncRegistryService');
|
||||
|
||||
export interface IStorageKeysSyncRegistryService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* All registered storage keys
|
||||
*/
|
||||
readonly storageKeys: ReadonlyArray<IStorageKey>;
|
||||
|
||||
/**
|
||||
* Event that is triggered when storage keys are changed
|
||||
*/
|
||||
readonly onDidChangeStorageKeys: Event<ReadonlyArray<IStorageKey>>;
|
||||
|
||||
/**
|
||||
* Register a storage key that has to be synchronized during sync.
|
||||
*/
|
||||
registerStorageKey(key: IStorageKey): void;
|
||||
|
||||
}
|
||||
|
||||
export class StorageKeysSyncRegistryService extends Disposable implements IStorageKeysSyncRegistryService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _storageKeys = new Map<string, IStorageKey>();
|
||||
get storageKeys(): ReadonlyArray<IStorageKey> { return values(this._storageKeys); }
|
||||
|
||||
private readonly _onDidChangeStorageKeys: Emitter<ReadonlyArray<IStorageKey>> = this._register(new Emitter<ReadonlyArray<IStorageKey>>());
|
||||
readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._register(toDisposable(() => this._storageKeys.clear()));
|
||||
}
|
||||
|
||||
registerStorageKey(storageKey: IStorageKey): void {
|
||||
if (!this._storageKeys.has(storageKey.key)) {
|
||||
this._storageKeys.set(storageKey.key, storageKey);
|
||||
this._onDidChangeStorageKeys.fire(this.storageKeys);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export interface IUserDataSyncStore {
|
||||
}
|
||||
|
||||
export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined {
|
||||
const value = productService[CONFIGURATION_SYNC_STORE_KEY] || configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY);
|
||||
const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY) || productService[CONFIGURATION_SYNC_STORE_KEY];
|
||||
if (value && value.url && value.authenticationProviderId) {
|
||||
return {
|
||||
url: joinPath(URI.parse(value.url), 'v1'),
|
||||
@@ -231,9 +231,13 @@ export interface ISyncExtension {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IStorageValue {
|
||||
version: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IGlobalState {
|
||||
argv: IStringDictionary<any>;
|
||||
storage: IStringDictionary<any>;
|
||||
storage: IStringDictionary<IStorageValue>;
|
||||
}
|
||||
|
||||
export const enum SyncStatus {
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
@@ -101,3 +103,51 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class StorageKeysSyncRegistryChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IStorageKeysSyncRegistryService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeStorageKeys': return this.service.onDidChangeStorageKeys;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve(this.service.storageKeys);
|
||||
case 'registerStorageKey': return Promise.resolve(this.service.registerStorageKey(args[0]));
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageKeysSyncRegistryChannelClient extends Disposable implements IStorageKeysSyncRegistryService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _storageKeys: ReadonlyArray<IStorageKey> = [];
|
||||
get storageKeys(): ReadonlyArray<IStorageKey> { return this._storageKeys; }
|
||||
private readonly _onDidChangeStorageKeys: Emitter<ReadonlyArray<IStorageKey>> = this._register(new Emitter<ReadonlyArray<IStorageKey>>());
|
||||
readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event;
|
||||
|
||||
constructor(private readonly channel: IChannel) {
|
||||
super();
|
||||
this.channel.call<IStorageKey[]>('_getInitialData').then(storageKeys => {
|
||||
this.updateStorageKeys(storageKeys);
|
||||
this._register(this.channel.listen<ReadonlyArray<IStorageKey>>('onDidChangeStorageKeys')(storageKeys => this.updateStorageKeys(storageKeys)));
|
||||
});
|
||||
}
|
||||
|
||||
private async updateStorageKeys(storageKeys: ReadonlyArray<IStorageKey>): Promise<void> {
|
||||
this._storageKeys = storageKeys;
|
||||
this._onDidChangeStorageKeys.fire(this.storageKeys);
|
||||
}
|
||||
|
||||
registerStorageKey(storageKey: IStorageKey): void {
|
||||
this.channel.call('registerStorageKey', [storageKey]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
|
||||
suite('GlobalStateMerge', () => {
|
||||
|
||||
test('merge when local and remote are same with one value', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const base = { 'b': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
const local = {};
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when new entry is added to remote from base and local has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['b']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when all entries are removed from base and local has not changed', async () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = {};
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['b', 'a']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'd' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'd' } });
|
||||
assert.deepEqual(actual.local.removed, ['b']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when new entries are added to local', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' }, 'c': { version: 1, value: 'c' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
|
||||
const local = { 'a': { version: 1, value: 'd' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['b']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge with single entry and local is empty', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' } };
|
||||
const local = {};
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' } };
|
||||
const local = { 'a': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote but not a registered key', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote but different version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated to remote but not a registered key', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is updated to remote but different version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is update with lower version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'c' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is update with higher version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 2, value: 'c' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when a local value is removed but not registered', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is removed with lower version', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is removed with higher version', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
});
|
||||
224
src/vs/platform/userDataSync/test/common/globalStateSync.test.ts
Normal file
224
src/vs/platform/userDataSync/test/common/globalStateSync.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, IGlobalState } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
|
||||
suite('GlobalStateSync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let testClient: UserDataSyncClient;
|
||||
let client2: UserDataSyncClient;
|
||||
|
||||
let testObject: GlobalStateSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
testClient = disposableStore.add(new UserDataSyncClient(server));
|
||||
await testClient.setUp(true);
|
||||
let storageKeysSyncRegistryService = testClient.instantiationService.get(IStorageKeysSyncRegistryService);
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'a', version: 1 });
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'b', version: 1 });
|
||||
testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.GlobalState) as GlobalStateSynchroniser;
|
||||
disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
|
||||
client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
storageKeysSyncRegistryService = client2.instantiationService.get(IStorageKeysSyncRegistryService);
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'a', version: 1 });
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'b', version: 1 });
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('first time sync - outgoing to server (no state)', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
await updateLocale(testClient);
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'globalState.argv.locale': { version: 1, value: 'en' }, 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('first time sync - incoming from server (no state)', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
await updateLocale(client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(await readLocale(testClient), 'en');
|
||||
});
|
||||
|
||||
test('first time sync when storage exists', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
await client2.sync();
|
||||
|
||||
updateStorage('b', 'value2', testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), 'value2');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('first time sync when storage exists - has conflicts', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
await client2.sync();
|
||||
|
||||
updateStorage('a', 'value2', client2);
|
||||
await testObject.sync();
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('sync adding a storage value', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
await testObject.sync();
|
||||
|
||||
updateStorage('b', 'value2', testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), 'value2');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('sync updating a storage value', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
await testObject.sync();
|
||||
|
||||
updateStorage('a', 'value2', testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value2');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('sync removing a storage value', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
updateStorage('b', 'value2', testClient);
|
||||
await testObject.sync();
|
||||
|
||||
removeStorage('b', testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), undefined);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('first time sync - push', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
updateStorage('b', 'value2', testClient);
|
||||
|
||||
await testObject.push();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('first time sync - pull', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
updateStorage('b', 'value2', client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.pull();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), 'value2');
|
||||
});
|
||||
|
||||
function parseGlobalState(content: string): IGlobalState {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
async function updateLocale(client: UserDataSyncClient): Promise<void> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
|
||||
}
|
||||
|
||||
function updateStorage(key: string, value: string, client: UserDataSyncClient): void {
|
||||
const storageService = client.instantiationService.get(IStorageService);
|
||||
storageService.store(key, value, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
function removeStorage(key: string, client: UserDataSyncClient): void {
|
||||
const storageService = client.instantiationService.get(IStorageService);
|
||||
storageService.remove(key, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
function readStorage(key: string, client: UserDataSyncClient): string | undefined {
|
||||
const storageService = client.instantiationService.get(IStorageService);
|
||||
return storageService.get(key, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
async function readLocale(client: UserDataSyncClient): Promise<string | undefined> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const content = await fileService.readFile(environmentService.argvResource);
|
||||
return JSON.parse(content.value.toString()).locale;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -68,7 +68,7 @@ suite('TestSynchronizer', () => {
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('status is syncing', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
|
||||
const actual: SyncStatus[] = [];
|
||||
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
|
||||
@@ -85,7 +85,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('status is set correctly when sync is finished', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
const actual: SyncStatus[] = [];
|
||||
@@ -97,7 +97,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('status is set correctly when sync has conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { status: SyncStatus.HasConflicts };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
@@ -110,7 +110,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('status is set correctly when sync has errors', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { error: true };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
@@ -127,7 +127,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('sync should not run if syncing already', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
const promise = Event.toPromise(testObject.onDoSyncCall.event);
|
||||
|
||||
testObject.sync();
|
||||
@@ -144,7 +144,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('sync should not run if disabled', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
client.instantiationService.get(IUserDataSyncEnablementService).setResourceEnablement(testObject.resource, false);
|
||||
|
||||
const actual: SyncStatus[] = [];
|
||||
@@ -157,7 +157,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('sync should not run if there are conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { status: SyncStatus.HasConflicts };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync();
|
||||
@@ -171,7 +171,7 @@ suite('TestSynchronizer', () => {
|
||||
});
|
||||
|
||||
test('request latest data on precondition failure', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings');
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
// Sync once
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync();
|
||||
|
||||
@@ -36,6 +36,7 @@ import { IAuthenticationTokenService } from 'vs/platform/authentication/common/a
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService';
|
||||
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
export class UserDataSyncClient extends Disposable {
|
||||
|
||||
@@ -92,6 +93,7 @@ export class UserDataSyncClient extends Disposable {
|
||||
this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService));
|
||||
this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService());
|
||||
this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService));
|
||||
this.instantiationService.stub(IStorageKeysSyncRegistryService, this.instantiationService.createInstance(StorageKeysSyncRegistryService));
|
||||
|
||||
this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService));
|
||||
this.instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
|
||||
|
||||
@@ -234,7 +234,6 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user