1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
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 interface IMergeResult {
local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
remote: IStringDictionary<IStorageValue> | null;
skipped: string[];
}
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
if (!remoteStorage) {
return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
}
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 { remote: null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
}
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 local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> } = { added: {}, removed: [], updated: {} };
const remote: IStringDictionary<IStorageValue> = objects.deepClone(remoteStorage);
const skipped: string[] = [];
// Added in remote
for (const key of values(baseToRemote.added)) {
const remoteValue = remoteStorage[key];
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.trace(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
logService.info(`GlobalState: Skipped adding ${key} in local 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)) {
const remoteValue = remoteStorage[key];
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.trace(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
logService.info(`GlobalState: Skipped updating ${key} in local 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
for (const key of values(baseToRemote.removed)) {
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
logService.trace(`GlobalState: Skipped removing ${key} in local 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];
}
}
// 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) {
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${localValue.version}' and remote version '${remoteValue.version} are not same.`);
continue;
}
remote[key] = localValue;
}
// Removed in local
for (const key of values(baseToLocal.removed)) {
// do not remove from remote if it is updated in remote
if (baseToRemote.updated.has(key)) {
continue;
}
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
// do not remove from remote if storage key is not found
if (!storageKey) {
skipped.push(key);
logService.trace(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
continue;
}
const remoteValue = remote[key];
// do not remove from remote if local data version is old
if (storageKey.version < remoteValue.version) {
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
continue;
}
// add to local if it was skipped before
if (previouslySkipped.indexOf(key) !== -1) {
local.added[key] = remote[key];
continue;
}
delete remote[key];
}
return { local, remote: areSame(remote, remoteStorage) ? null : remote, skipped };
}
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from);
const toKeys = Object.keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const value1 = from[key];
const value2 = to[key];
if (!objects.equals(value1, value2)) {
updated.add(key);
}
}
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;
}
|