src/vs/platform/userDataSync/common/abstractJsonSynchronizer.ts

276 LOC · 262 covered · 14 uncovered · 65 ranges · 585 concepts · 26 introducers · 348 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- abstractSynchronizer.ts ×49
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { IConfigurationService } from '../../configuration/common/configuration.js';
9 > import { IEnvironmentService } from '../../environment/common/environment.js';
10 > import { IFileService } from '../../files/common/files.js';
11 > import { IStorageService } from '../../storage/common/storage.js';
12 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
13 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
14 > import { IUserDataProfile } from '../../userDataProfile/common/userDataProfile.js';
15 > import { AbstractFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from './abstractSynchronizer.js';
16 > import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, USER_DATA_SYNC_SCHEME, SyncResource } from './userDataSync.js';
17 >
18 > export interface IJsonResourcePreview extends IFileResourcePreview {
19 > previewResult: IMergeResult;
20 > }
21 >
22 > export abstract class AbstractJsonSynchronizer extends AbstractFileSynchroniser implements IUserDataSynchroniser {
23 >
24 > protected readonly version: number = 1;
25 > private readonly previewResource: URI;
26 > private readonly baseResource: URI;
27 > private readonly localResource: URI;
28 > private readonly remoteResource: URI;
29 > private readonly acceptedResource: URI;
30 >
31 > constructor(
32 > fileResource: URI, userDataSyncService.ts ×19
33 > syncResourceMetadata: { syncResource: SyncResource; profile: IUserDataProfile },
34 > collection: string | undefined,
35 > previewFileName: string,
36 > @IFileService fileService: IFileService,
37 > @IEnvironmentService environmentService: IEnvironmentService,
38 > @IStorageService storageService: IStorageService,
39 > @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
40 > @IUserDataSyncLocalStoreService userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
41 > @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
42 > @ITelemetryService telemetryService: ITelemetryService,
43 > @IUserDataSyncLogService logService: IUserDataSyncLogService,
44 > @IConfigurationService configurationService: IConfigurationService,
45 > @IUriIdentityService uriIdentityService: IUriIdentityService,
46 > ) {
47 > super(fileResource, syncResourceMetadata, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
48 >
49 > this.previewResource = this.extUri.joinPath(this.syncPreviewFolder, previewFileName);
50 > this.baseResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
51 > this.localResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
52 > this.remoteResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
53 > this.acceptedResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
54 > }
56 > protected abstract getContentFromSyncContent(syncContent: string): string | null;
57 > protected abstract toSyncContent(content: string | null): object;
58 >
59 > protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise<IJsonResourcePreview[]> {
60 > const remoteContent = remoteUserData.syncData ? this.getContentFromSyncContent(remoteUserData.syncData.content) : null; abstractJsonSynchronizer.ts ×8
61 >
62 > // Use remote data as last sync data if last sync data does not exist and remote data is from same machine
63 > lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData;
64 > const lastSyncContent: string | null = lastSyncUserData?.syncData ? this.getContentFromSyncContent(lastSyncUserData.syncData.content) : null;
65 >
66 > // Get file content last to get the latest
67 > const fileContent = await this.getLocalFileContent();
68 >
69 > let content: string | null = null;
70 > let hasLocalChanged: boolean = false;
71 > let hasRemoteChanged: boolean = false;
72 > let hasConflicts: boolean = false;
73 >
74 > if (remoteUserData.syncData) {
75 > const localContent = fileContent ? fileContent.value.toString() : null; abstractJsonSynchronizer.ts ×3
76 > if (!lastSyncContent // First time sync
77 > || lastSyncContent !== localContent // Local has forwarded abstractJsonSynchronizer.ts ×1
78 > || lastSyncContent !== remoteContent // Remote has forwarded abstractJsonSynchronizer.ts ×1
80 > this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ${this.syncResource.syncResource} with local ${this.syncResource.syncResource}...`); abstractJsonSynchronizer.ts ×1
81 > const result = this.merge(localContent, remoteContent, lastSyncContent);
82 > content = result.content;
83 > hasConflicts = result.hasConflicts;
84 > hasLocalChanged = result.hasLocalChanged;
85 > hasRemoteChanged = result.hasRemoteChanged;
86 > }
89 > // First time syncing to remote
90 > else if (fileContent) {
91 > this.logService.trace(`${this.syncResourceLogLabel}: Remote ${this.syncResource.syncResource} does not exist. Synchronizing ${this.syncResource.syncResource} for the first time.`); abstractJsonSynchronizer.ts ×5
92 > content = fileContent.value.toString();
93 > hasRemoteChanged = true;
94 > }
96 > const previewResult: IMergeResult = {
97 > content: hasConflicts ? lastSyncContent : content,
98 > localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
99 > remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
100 > hasConflicts
101 > };
102 >
103 > const localContent = fileContent ? fileContent.value.toString() : null;
104 > return [{
105 > fileContent,
106 >
107 > baseResource: this.baseResource,
108 > baseContent: lastSyncContent,
109 >
110 > localResource: this.localResource,
111 > localContent,
112 > localChange: previewResult.localChange,
113 >
114 > remoteResource: this.remoteResource,
115 > remoteContent,
116 > remoteChange: previewResult.remoteChange,
117 >
118 > previewResource: this.previewResource,
119 > previewResult,
120 > acceptedResource: this.acceptedResource,
121 > }];
122 > }
124 > protected async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise<boolean> {
125 > const lastSyncContent: string | null = lastSyncUserData?.syncData ? this.getContentFromSyncContent(lastSyncUserData.syncData.content) : null; abstractJsonSynchronizer.ts ×2
126 > if (lastSyncContent === null) {
127 return true;
128 }
130 > const fileContent = await this.getLocalFileContent();
131 > const localContent = fileContent ? fileContent.value.toString() : null;
132 > const result = this.merge(localContent, lastSyncContent, lastSyncContent);
133 > return result.hasLocalChanged || result.hasRemoteChanged;
134 > }
136 > protected async getMergeResult(resourcePreview: IJsonResourcePreview, token: CancellationToken): Promise<IMergeResult> {
137 > return resourcePreview.previewResult; abstractJsonSynchronizer.ts ×5
138 > }
140 > protected async getAcceptResult(resourcePreview: IJsonResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
141 > /* Accept local resource */ abstractJsonSynchronizer.ts ×3
142 > if (this.extUri.isEqual(resource, this.localResource)) {
144 > content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
145 > localChange: Change.None,
146 > remoteChange: Change.Modified,
147 > };
148 > }
150 > /* Accept remote resource */
151 > if (this.extUri.isEqual(resource, this.remoteResource)) {
153 > content: resourcePreview.remoteContent,
154 > localChange: Change.Modified,
155 > remoteChange: Change.None,
156 > };
157 > }
159 > /* Accept preview resource */
160 > if (this.extUri.isEqual(resource, this.previewResource)) {
161 > if (content === undefined) {
162 > return {
163 > content: resourcePreview.previewResult.content,
164 > localChange: resourcePreview.previewResult.localChange,
165 > remoteChange: resourcePreview.previewResult.remoteChange,
166 > };
167 > } else {
169 > content,
170 > localChange: Change.Modified,
171 > remoteChange: Change.Modified,
172 > };
173 > }
175
176 throw new Error(`Invalid Resource: ${resource.toString()}`);
179 > protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IJsonResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
180 > const { fileContent } = resourcePreviews[0][0]; abstractJsonSynchronizer.ts ×8
181 > const { content, localChange, remoteChange } = resourcePreviews[0][1];
182 >
183 > if (localChange === Change.None && remoteChange === Change.None) {
184 > this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ${this.syncResource.syncResource}.`); abstractJsonSynchronizer.ts ×1
185 > }
187 > if (localChange !== Change.None) {
188 > this.logService.trace(`${this.syncResourceLogLabel}: Updating local ${this.syncResource.syncResource}...`); abstractJsonSynchronizer.ts ×4
189 > if (fileContent) {
190 > await this.backupLocal(JSON.stringify(this.toSyncContent(fileContent.value.toString()))); abstractJsonSynchronizer.ts ×1
191 > }
192 > if (content) { abstractJsonSynchronizer.ts ×4
193 > await this.updateLocalFileContent(content, fileContent, force); abstractJsonSynchronizer.ts ×3
195 > await this.deleteLocalFile(); abstractSynchronizer.ts ×2
196 > }
197 > this.logService.info(`${this.syncResourceLogLabel}: Updated local ${this.syncResource.syncResource}`); abstractJsonSynchronizer.ts ×4
198 > }
200 > if (remoteChange !== Change.None) {
201 > this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ${this.syncResource.syncResource}...`); abstractJsonSynchronizer.ts ×5
202 > const remoteContents = JSON.stringify(this.toSyncContent(content));
203 > remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
204 > this.logService.info(`${this.syncResourceLogLabel}: Updated remote ${this.syncResource.syncResource}`); abstractJsonSynchronizer.ts ×1
205 > }
207 > // Delete the preview
208 > try {
209 > await this.fileService.del(this.previewResource);
210 > } catch (e) { /* ignore */ }
211 >
212 > if (lastSyncUserData?.ref !== remoteUserData.ref) { abstractJsonSynchronizer.ts ×8
213 > this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ${this.syncResource.syncResource}...`); abstractJsonSynchronizer.ts ×2
214 > await this.updateLastSyncUserData(remoteUserData);
215 > this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ${this.syncResource.syncResource}`);
216 > }
219 > async hasLocalData(): Promise<boolean> {
220 return this.fileService.exists(this.file);
221 }
223 > async resolveContent(uri: URI): Promise<string | null> {
224 > if (this.extUri.isEqual(this.remoteResource, uri) abstractJsonSynchronizer.ts ×3
225 || this.extUri.isEqual(this.baseResource, uri)
226 || this.extUri.isEqual(this.localResource, uri)
227 || this.extUri.isEqual(this.acceptedResource, uri)
229 > return this.resolvePreviewContent(uri);
230 > }
231 return null;
234 > private merge(originalLocalContent: string | null, originalRemoteContent: string | null, baseContent: string | null): {
235 > content: string | null; abstractJsonSynchronizer.ts ×6
236 > hasLocalChanged: boolean;
237 > hasRemoteChanged: boolean;
238 > hasConflicts: boolean;
239 > } {
240 >
241 > /* no changes */
242 > if (originalLocalContent === null && originalRemoteContent === null && baseContent === null) {
243 return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
244 }
246 > // Normalize nulls to empty strings for easier comparison
247 > originalRemoteContent = originalRemoteContent ?? '';
248 > originalLocalContent = originalLocalContent ?? '';
249 > baseContent = baseContent ?? '';
250 >
251 > /* no changes */
252 > if (originalLocalContent === originalRemoteContent) {
253 > return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false }; abstractJsonSynchronizer.ts ×1
254 > }
256 > const localForwarded = baseContent !== originalLocalContent;
257 > const remoteForwarded = baseContent !== originalRemoteContent;
258 >
259 > /* no changes */
260 > if (!localForwarded && !remoteForwarded) { abstractJsonSynchronizer.ts ×6
261 return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
262 }
264 > /* local has changed and remote has not */
265 > if (localForwarded && !remoteForwarded) { abstractJsonSynchronizer.ts ×6
266 > return { content: originalLocalContent, hasRemoteChanged: true, hasLocalChanged: false, hasConflicts: false }; abstractJsonSynchronizer.ts ×1
267 > }
269 > /* remote has changed and local has not */
270 > if (remoteForwarded && !localForwarded) { abstractJsonSynchronizer.ts ×6
271 > return { content: originalRemoteContent, hasLocalChanged: true, hasRemoteChanged: false, hasConflicts: false }; abstractJsonSynchronizer.ts ×3
272 > }
274 > return { content: originalLocalContent, hasLocalChanged: true, hasRemoteChanged: true, hasConflicts: true };