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

948 LOC · 638 covered · 310 uncovered · 165 ranges · 585 concepts · 29 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 { equals } from '../../../base/common/arrays.js';
7 > import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from '../../../base/common/async.js';
8 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
9 > import { toErrorMessage } from '../../../base/common/errorMessage.js';
10 > import { Emitter, Event } from '../../../base/common/event.js';
11 > import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
12 > import { isEqual } from '../../../base/common/resources.js';
13 > import { isBoolean, isUndefined } from '../../../base/common/types.js';
14 > import { URI } from '../../../base/common/uri.js';
15 > import { generateUuid } from '../../../base/common/uuid.js';
16 > import { IConfigurationService } from '../../configuration/common/configuration.js';
17 > import { IExtensionGalleryService } from '../../extensionManagement/common/extensionManagement.js';
18 > import { IFileService } from '../../files/common/files.js';
19 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
20 > import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
21 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
22 > import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
23 > import { ExtensionsSynchroniser } from './extensionsSync.js';
24 > import { GlobalStateSynchroniser } from './globalStateSync.js';
25 > import { KeybindingsSynchroniser } from './keybindingsSync.js';
26 > import { PromptsSynchronizer } from './promptsSync/promptsSync.js';
27 > import { SettingsSynchroniser } from './settingsSync.js';
28 > import { SnippetsSynchroniser } from './snippetsSync.js';
29 > import { TasksSynchroniser } from './tasksSync.js';
30 > import { McpSynchroniser } from './mcpSync.js';
31 > import { UserDataProfilesManifestSynchroniser } from './userDataProfilesManifestSync.js';
32 > import {
33 > ALL_SYNC_RESOURCES, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError,
34 > IUserDataSyncResource, ISyncResourceHandle, IUserDataSyncTask, ISyncUserDataProfile, IUserDataManifest, IUserDataSyncConfiguration,
35 > IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService,
36 > SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE, IUserDataSyncResourceProviderService, IUserDataSyncActivityData, IUserDataSyncLocalStoreService,
37 > IUserDataSyncLatestData,
38 > IUserData,
39 > isUserDataManifest,
40 > } from './userDataSync.js';
41 >
42 > type SyncErrorClassification = {
43 > owner: 'sandy081';
44 > comment: 'Information about the error that occurred while syncing';
45 > code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' };
46 > service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service for which this error has occurred' };
47 > serverCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service error code' };
48 > url?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource URL for which this error has occurred' };
49 > resource?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource for which this error has occurred' };
50 > executionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync execution id for which this error has occurred' };
51 > };
52 >
53 > type SyncErrorEvent = {
54 > code: string;
55 > service: string;
56 > serverCode?: string;
57 > url?: string;
58 > resource?: string;
59 > executionId?: string;
60 > };
61 >
62 > const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
63 >
64 > export class UserDataSyncService extends Disposable implements IUserDataSyncService {
65 >
66 > _serviceBrand: undefined;
67 >
68 > private _status: SyncStatus = SyncStatus.Uninitialized;
69 > get status(): SyncStatus { return this._status; }
70 > private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
71 > readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
72 >
73 > private _onDidChangeLocal = this._register(new Emitter<SyncResource>());
74 > readonly onDidChangeLocal = this._onDidChangeLocal.event;
75 >
76 > private _conflicts: IUserDataSyncResourceConflicts[] = [];
77 > get conflicts(): IUserDataSyncResourceConflicts[] { return this._conflicts; }
78 > private _onDidChangeConflicts = this._register(new Emitter<IUserDataSyncResourceConflicts[]>());
79 > readonly onDidChangeConflicts = this._onDidChangeConflicts.event;
80 >
81 > private _syncErrors: IUserDataSyncResourceError[] = [];
82 > private _onSyncErrors = this._register(new Emitter<IUserDataSyncResourceError[]>());
83 > readonly onSyncErrors = this._onSyncErrors.event;
84 >
85 > private _lastSyncTime: number | undefined = undefined;
86 > get lastSyncTime(): number | undefined { return this._lastSyncTime; }
87 > private _onDidChangeLastSyncTime: Emitter<number> = this._register(new Emitter<number>());
88 > readonly onDidChangeLastSyncTime: Event<number> = this._onDidChangeLastSyncTime.event;
89 >
90 > private _onDidResetLocal = this._register(new Emitter<void>());
91 > readonly onDidResetLocal = this._onDidResetLocal.event;
92 >
93 > private _onDidResetRemote = this._register(new Emitter<void>());
94 > readonly onDidResetRemote = this._onDidResetRemote.event;
95 >
96 > private activeProfileSynchronizers = new Map<string, [ProfileSynchronizer, IDisposable]>();
97 >
98 > constructor(
99 > @IFileService private readonly fileService: IFileService, userDataSyncStoreService.ts ×36
100 > @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
101 > @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
102 > @IInstantiationService private readonly instantiationService: IInstantiationService,
103 > @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
104 > @ITelemetryService private readonly telemetryService: ITelemetryService,
105 > @IStorageService private readonly storageService: IStorageService,
106 > @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
107 > @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
108 > @IUserDataSyncResourceProviderService private readonly userDataSyncResourceProviderService: IUserDataSyncResourceProviderService,
109 > @IUserDataSyncLocalStoreService private readonly userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
110 > ) {
111 > super();
112 > this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized;
113 > this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION, undefined);
114 > this._register(toDisposable(() => this.clearActiveProfileSynchronizers()));
115 >
116 > this._register(new RunOnceScheduler(() => this.cleanUpStaleStorageData(), 5 * 1000 /* after 5s */)).schedule();
117 > }
119 > async createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise<IUserDataSyncTask> {
120 > this.checkEnablement(); userDataSyncService.ts ×22
121 >
122 > this.logService.info('Sync started.');
123 > const startTime = new Date().getTime();
124 > const executionId = generateUuid();
125 > try {
126 > const syncHeaders = createSyncHeaders(executionId);
127 > if (disableCache) {
128 > syncHeaders['Cache-Control'] = 'no-cache'; userDataSyncService.ts ×1
129 > }
130 > manifest = await this.userDataSyncStoreService.manifest(manifest, syncHeaders); userDataSyncService.ts ×22
131 > } catch (error) {
132 const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
133 reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
134 throw userDataSyncError;
135 }
137 > const executed = false;
138 > const that = this;
139 > let cancellablePromise: CancelablePromise<void> | undefined;
140 > return {
141 > manifest,
142 > async run(): Promise<void> {
143 > if (executed) {
144 throw new Error('Can run a task only once');
145 }
146 > cancellablePromise = createCancelablePromise(token => that.sync(manifest, false, executionId, token)); userDataSyncService.ts ×22
147 > await cancellablePromise.finally(() => cancellablePromise = undefined);
148 > that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); extensionsSync.ts ×17
149 > that.updateLastSyncTime();
151 > stop(): Promise<void> {
152 > cancellablePromise?.cancel(); userDataSyncService.ts ×6
153 > return that.stop();
154 > }
156 > }
158 > async createManualSyncTask(): Promise<IUserDataManualSyncTask> {
159 this.checkEnablement();
160
161 if (this.userDataSyncEnablementService.isEnabled()) {
162 throw new UserDataSyncError('Cannot start manual sync when sync is enabled', UserDataSyncErrorCode.LocalError);
163 }
164
165 this.logService.info('Sync started.');
166 const startTime = new Date().getTime();
167 const executionId = generateUuid();
168 const syncHeaders = createSyncHeaders(executionId);
169 let latestUserDataOrManifest: IUserDataSyncLatestData | IUserDataManifest | null;
170 try {
171 latestUserDataOrManifest = await this.userDataSyncStoreService.getLatestData(syncHeaders);
172 } catch (error) {
173 const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
174 this.telemetryService.publicLog2<SyncErrorEvent, SyncErrorClassification>('sync.download.latest',
175 {
176 code: userDataSyncError.code,
177 serverCode: userDataSyncError instanceof UserDataSyncStoreError ? String(userDataSyncError.serverCode) : undefined,
178 url: userDataSyncError instanceof UserDataSyncStoreError ? userDataSyncError.url : undefined,
179 resource: userDataSyncError.resource,
180 executionId,
181 service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString()
182 });
183
184 // Fallback to manifest in stable
185 try {
186 latestUserDataOrManifest = await this.userDataSyncStoreService.manifest(null, syncHeaders);
187 } catch (error) {
188 const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
189 reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
190 throw userDataSyncError;
191 }
192 }
193
194 /* Manual sync shall start on clean local state */
195 await this.resetLocal();
196
197 const that = this;
198 const cancellableToken = new CancellationTokenSource();
199 return {
200 id: executionId,
201 async merge(): Promise<void> {
202 return that.sync(latestUserDataOrManifest, true, executionId, cancellableToken.token);
203 },
204 async apply(): Promise<void> {
205 try {
206 try {
207 await that.applyManualSync(latestUserDataOrManifest, executionId, cancellableToken.token);
208 } catch (error) {
209 if (UserDataSyncError.toUserDataSyncError(error).code === UserDataSyncErrorCode.MethodNotFound) {
210 that.logService.info('Client is making invalid requests. Cleaning up data...');
211 await that.cleanUpRemoteData();
212 that.logService.info('Applying manual sync again...');
213 await that.applyManualSync(latestUserDataOrManifest, executionId, cancellableToken.token);
214 } else {
215 throw error;
216 }
217 }
218 } catch (error) {
219 that.logService.error(error);
220 throw error;
221 }
222 that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
223 that.updateLastSyncTime();
224 },
225 async stop(): Promise<void> {
226 cancellableToken.cancel();
227 await that.stop();
228 await that.resetLocal();
229 }
230 };
231 }
233 > private async sync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<void> {
234 > this._syncErrors = []; userDataSyncService.ts ×22
235 > try {
236 > if (this.status !== SyncStatus.HasConflicts) {
237 > this.setStatus(SyncStatus.Syncing);
238 > }
239 >
240 > // Sync Default Profile First
241 > const defaultProfileSynchronizer = this.getOrCreateActiveProfileSynchronizer(this.userDataProfilesService.defaultProfile, undefined);
242 > this._syncErrors.push(...await this.syncProfile(defaultProfileSynchronizer, manifestOrLatestData, preview, executionId, token));
244 > // Sync other profiles
245 > const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles);
246 > if (userDataProfileManifestSynchronizer) {
247 > const syncProfiles = (await (userDataProfileManifestSynchronizer as UserDataProfilesManifestSynchroniser).getLastSyncedProfiles()) || [];
248 > if (token.isCancellationRequested) {
250 > }
251 > await this.syncRemoteProfiles(syncProfiles, manifestOrLatestData, preview, executionId, token); extensionsSync.ts ×17
252 > }
253 > } finally { userDataSyncService.ts ×22
254 > if (this.status !== SyncStatus.HasConflicts) {
255 > this.setStatus(SyncStatus.Idle);
256 > }
257 > this._onSyncErrors.fire(this._syncErrors);
258 > }
259 > }
261 > private async syncRemoteProfiles(remoteProfiles: ISyncUserDataProfile[], manifest: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<void> {
262 > for (const syncProfile of remoteProfiles) { extensionsSync.ts ×17
263 > if (token.isCancellationRequested) { userDataSyncService.ts ×4
264 return;
265 }
266 > const profile = this.userDataProfilesService.profiles.find(p => p.id === syncProfile.id); userDataSyncService.ts ×4
267 > if (!profile) {
268 this.logService.error(`Profile with id:${syncProfile.id} and name: ${syncProfile.name} does not exist locally to sync.`);
269 continue;
270 }
271 > this.logService.info('Syncing profile.', syncProfile.name); userDataSyncService.ts ×4
272 > const profileSynchronizer = this.getOrCreateActiveProfileSynchronizer(profile, syncProfile);
273 > this._syncErrors.push(...await this.syncProfile(profileSynchronizer, manifest, preview, executionId, token));
274 > }
275 > // Dispose & Delete profile synchronizers which do not exist anymore extensionsSync.ts ×17
276 > for (const [key, profileSynchronizerItem] of this.activeProfileSynchronizers.entries()) {
277 > if (this.userDataProfilesService.profiles.some(p => p.id === profileSynchronizerItem[0].profile.id)) {
278 > continue;
279 > }
280 > await profileSynchronizerItem[0].resetLocal(); userDataProfilesManifestSync.ts ×3
281 > profileSynchronizerItem[1].dispose();
282 > this.activeProfileSynchronizers.delete(key);
283 > }
286 > private async applyManualSync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, executionId: string, token: CancellationToken): Promise<void> {
287 try {
288 this.setStatus(SyncStatus.Syncing);
289 const profileSynchronizers = this.getActiveProfileSynchronizers();
290 for (const profileSynchronizer of profileSynchronizers) {
291 if (token.isCancellationRequested) {
292 return;
293 }
294 await profileSynchronizer.apply(executionId, token);
295 }
296
297 const defaultProfileSynchronizer = profileSynchronizers.find(s => s.profile.isDefault);
298 if (!defaultProfileSynchronizer) {
299 return;
300 }
301
302 const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles);
303 if (!userDataProfileManifestSynchronizer) {
304 return;
305 }
306
307 // Sync remote profiles which are not synced locally
308 const remoteProfiles = (await (userDataProfileManifestSynchronizer as UserDataProfilesManifestSynchroniser).getRemoteSyncedProfiles(getRefOrUserData(manifestOrLatestData, undefined, SyncResource.Profiles) ?? null)) || [];
309 const remoteProfilesToSync = remoteProfiles.filter(remoteProfile => profileSynchronizers.every(s => s.profile.id !== remoteProfile.id));
310 if (remoteProfilesToSync.length) {
311 await this.syncRemoteProfiles(remoteProfilesToSync, manifestOrLatestData, false, executionId, token);
312 }
313 } finally {
314 this.setStatus(SyncStatus.Idle);
315 }
316 }
318 > private async syncProfile(profileSynchronizer: ProfileSynchronizer, manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<IUserDataSyncResourceError[]> {
319 > const errors = await profileSynchronizer.sync(manifestOrLatestData, preview, executionId, token); userDataSyncService.ts ×22
320 > return errors.map(([syncResource, error]) => ({ profile: profileSynchronizer.profile, syncResource, error })); extensionsSync.ts ×17
323 > private async stop(): Promise<void> {
324 > if (this.status !== SyncStatus.Idle) { userDataSyncService.ts ×6
325 > await Promise.allSettled(this.getActiveProfileSynchronizers().map(profileSynchronizer => profileSynchronizer.stop()));
326 > }
327 > }
329 > async resolveContent(resource: URI): Promise<string | null> {
330 const content = await this.userDataSyncResourceProviderService.resolveContent(resource);
331 if (content) {
332 return content;
333 }
334 for (const profileSynchronizer of this.getActiveProfileSynchronizers()) {
335 for (const synchronizer of profileSynchronizer.enabled) {
336 const content = await synchronizer.resolveContent(resource);
337 if (content) {
338 return content;
339 }
340 }
341 }
342 return null;
343 }
345 > async replace(syncResourceHandle: ISyncResourceHandle): Promise<void> {
346 this.checkEnablement();
347
348 const profileSyncResource = this.userDataSyncResourceProviderService.resolveUserDataSyncResource(syncResourceHandle);
349 if (!profileSyncResource) {
350 return;
351 }
352
353 const content = await this.resolveContent(syncResourceHandle.uri);
354 if (!content) {
355 return;
356 }
357
358 await this.performAction(profileSyncResource.profile, async synchronizer => {
359 if (profileSyncResource.syncResource === synchronizer.resource) {
360 await synchronizer.replace(content);
361 return true;
362 }
363 return undefined;
364 });
365
366 return;
367 }
369 > async accept(syncResource: IUserDataSyncResource, resource: URI, content: string | null | undefined, apply: boolean | { force: boolean }): Promise<void> {
370 this.checkEnablement();
371
372 await this.performAction(syncResource.profile, async synchronizer => {
373 if (syncResource.syncResource === synchronizer.resource) {
374 await synchronizer.accept(resource, content);
375 if (apply) {
376 await synchronizer.apply(isBoolean(apply) ? false : apply.force, createSyncHeaders(generateUuid()));
377 }
378 return true;
379 }
380 return undefined;
381 });
382 }
384 > async hasLocalData(): Promise<boolean> {
385 const result = await this.performAction(this.userDataProfilesService.defaultProfile, async synchronizer => {
386 // skip global state synchronizer
387 if (synchronizer.resource !== SyncResource.GlobalState && await synchronizer.hasLocalData()) {
388 return true;
389 }
390 return undefined;
391 });
392 return !!result;
393 }
395 > async hasPreviouslySynced(): Promise<boolean> {
396 > const result = await this.performAction(this.userDataProfilesService.defaultProfile, async synchronizer => { userDataSyncService.ts ×9
397 > if (await synchronizer.hasPreviouslySynced()) {
398 > return true; userDataSyncService.ts ×3
399 > }
400 > return undefined; userDataSyncService.ts ×3
402 > return !!result;
403 > }
405 > async reset(): Promise<void> {
406 > this.checkEnablement(); userDataSyncService.ts ×3
407 > await this.resetRemote();
408 > await this.resetLocal();
409 > }
411 > async resetRemote(): Promise<void> {
412 > this.checkEnablement(); userDataSyncService.ts ×3
413 > try {
414 > await this.userDataSyncStoreService.clear();
415 > this.logService.info('Cleared data on server');
416 > } catch (e) {
417 this.logService.error(e);
418 }
419 > this._onDidResetRemote.fire(); userDataSyncService.ts ×3
420 > }
422 > async resetLocal(): Promise<void> {
423 > this.checkEnablement(); userDataSyncService.ts ×2
424 > this._lastSyncTime = undefined;
425 > this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION);
426 > for (const [synchronizer] of this.activeProfileSynchronizers.values()) {
427 > try {
428 > await synchronizer.resetLocal();
429 > } catch (e) {
430 this.logService.error(e);
431 }
433 > this.clearActiveProfileSynchronizers();
434 > this._onDidResetLocal.fire();
435 > this.logService.info('Did reset the local sync state.');
436 > }
438 > private async cleanUpStaleStorageData(): Promise<void> {
439 > const allKeys = this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE); userDataSyncService.ts ×5
440 > const lastSyncProfileKeys: [string, string][] = [];
441 > for (const key of allKeys) {
442 > if (!key.endsWith('.lastSyncUserData')) {
443 > continue;
444 > }
445 > const segments = key.split('.'); userDataSyncService.ts ×1
446 > if (segments.length === 3) {
447 > lastSyncProfileKeys.push([key, segments[0]]); userDataSyncService.ts ×6
448 > }
450 > if (!lastSyncProfileKeys.length) {
452 > }
454 > const disposables = new DisposableStore();
455 >
456 > try {
457 > let defaultProfileSynchronizer = this.activeProfileSynchronizers.get(this.userDataProfilesService.defaultProfile.id)?.[0];
458 > if (!defaultProfileSynchronizer) { userDataSyncService.ts ×5
459 defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, this.userDataProfilesService.defaultProfile, undefined));
460 }
461 > const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles) as UserDataProfilesManifestSynchroniser; userDataSyncService.ts ×6
462 > if (!userDataProfileManifestSynchronizer) {
463 return;
464 }
465 > const lastSyncedProfiles = await userDataProfileManifestSynchronizer.getLastSyncedProfiles(); userDataSyncService.ts ×6
466 > const lastSyncedCollections = lastSyncedProfiles?.map(p => p.collection) ?? []; userDataSyncService.ts ×5
467 > for (const [key, collection] of lastSyncProfileKeys) {
468 > if (!lastSyncedCollections.includes(collection)) { userDataSyncService.ts ×6
469 this.logService.info(`Removing last sync state for stale profile: ${collection}`);
470 this.storageService.remove(key, StorageScope.APPLICATION);
471 }
473 > } finally {
474 > disposables.dispose();
475 > }
478 > async cleanUpRemoteData(): Promise<void> {
479 const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles();
480 const remoteProfileCollections = remoteProfiles.map(profile => profile.collection);
481 const allCollections = await this.userDataSyncStoreService.getAllCollections();
482 const redundantCollections = allCollections.filter(c => !remoteProfileCollections.includes(c));
483 if (redundantCollections.length) {
484 this.logService.info(`Deleting ${redundantCollections.length} redundant collections on server`);
485 await Promise.allSettled(redundantCollections.map(collectionId => this.userDataSyncStoreService.deleteCollection(collectionId)));
486 this.logService.info(`Deleted redundant collections on server`);
487 }
488 const updatedRemoteProfiles = remoteProfiles.filter(profile => allCollections.includes(profile.collection));
489 if (updatedRemoteProfiles.length !== remoteProfiles.length) {
490 const profileManifestSynchronizer = this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.userDataProfilesService.defaultProfile, undefined);
491 try {
492 this.logService.info('Resetting the last synced state of profiles');
493 await profileManifestSynchronizer.resetLocal();
494 this.logService.info('Did reset the last synced state of profiles');
495 this.logService.info(`Updating remote profiles with invalid collections on server`);
496 await profileManifestSynchronizer.updateRemoteProfiles(updatedRemoteProfiles, null);
497 this.logService.info(`Updated remote profiles on server`);
498 } finally {
499 profileManifestSynchronizer.dispose();
500 }
501 }
502 }
504 > async saveRemoteActivityData(location: URI): Promise<void> {
505 this.checkEnablement();
506 const data = await this.userDataSyncStoreService.getActivityData();
507 await this.fileService.writeFile(location, data);
508 }
510 > async extractActivityData(activityDataResource: URI, location: URI): Promise<void> {
511 const content = (await this.fileService.readFile(activityDataResource)).value.toString();
512 const activityData: IUserDataSyncActivityData = JSON.parse(content);
513
514 if (activityData.resources) {
515 for (const resource in activityData.resources) {
516 for (const version of activityData.resources[resource]) {
517 await this.userDataSyncLocalStoreService.writeResource(resource as SyncResource, version.content, new Date(version.created * 1000), undefined, location);
518 }
519 }
520 }
521
522 if (activityData.collections) {
523 for (const collection in activityData.collections) {
524 for (const resource in activityData.collections[collection].resources) {
525 for (const version of activityData.collections[collection].resources?.[resource] ?? []) {
526 await this.userDataSyncLocalStoreService.writeResource(resource as SyncResource, version.content, new Date(version.created * 1000), collection, location);
527 }
528 }
529 }
530 }
531 }
533 > private async performAction<T>(profile: IUserDataProfile, action: (synchroniser: IUserDataSynchroniser) => Promise<T | undefined>): Promise<T | null> {
534 > const disposables = new DisposableStore(); userDataSyncService.ts ×9
535 > try {
536 > const activeProfileSyncronizer = this.activeProfileSynchronizers.get(profile.id);
537 > if (activeProfileSyncronizer) {
538 > const result = await this.performActionWithProfileSynchronizer(activeProfileSyncronizer[0], action, disposables); userDataSyncService.ts ×3
539 > return isUndefined(result) ? null : result;
540 > }
542 > if (profile.isDefault) {
543 > const defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, undefined));
544 > const result = await this.performActionWithProfileSynchronizer(defaultProfileSynchronizer, action, disposables);
545 > return isUndefined(result) ? null : result;
546 > }
547
548 const userDataProfileManifestSynchronizer = disposables.add(this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, profile, undefined));
549 const manifest = await this.userDataSyncStoreService.manifest(null);
550 > const syncProfiles = (await userDataProfileManifestSynchronizer.getRemoteSyncedProfiles(manifest?.latest?.profiles ?? null)) || []; userDataSyncService.ts ×9
551 > const syncProfile = syncProfiles.find(syncProfile => syncProfile.id === profile.id);
552 > if (syncProfile) {
553 const profileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, syncProfile.collection));
554 const result = await this.performActionWithProfileSynchronizer(profileSynchronizer, action, disposables);
555 return isUndefined(result) ? null : result;
556 }
557
558 return null;
559 > } finally { userDataSyncService.ts ×9
560 > disposables.dispose();
561 > }
562 > }
564 > private async performActionWithProfileSynchronizer<T>(profileSynchronizer: ProfileSynchronizer, action: (synchroniser: IUserDataSynchroniser) => Promise<T | undefined>, disposables: DisposableStore): Promise<T | undefined> {
565 > const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.reduce<(IUserDataSynchroniser & IDisposable)[]>((synchronizers, syncResource) => { userDataSyncService.ts ×9
566 if (syncResource !== SyncResource.WorkspaceState) {
567 synchronizers.push(disposables.add(profileSynchronizer.createSynchronizer(syncResource)));
568 }
569 return synchronizers;
571 > for (const synchronizer of allSynchronizers) {
572 > const result = await action(synchronizer);
573 > if (!isUndefined(result)) {
574 > return result; userDataSyncService.ts ×3
575 > }
577 > return undefined; userDataSyncService.ts ×3
580 > private setStatus(status: SyncStatus): void {
581 > const oldStatus = this._status; userDataSyncService.ts ×19
582 > if (this._status !== status) {
583 > this._status = status;
584 > this._onDidChangeStatus.fire(status);
585 > if (oldStatus === SyncStatus.HasConflicts) {
586 > this.updateLastSyncTime(); userDataSyncService.ts ×1
587 > }
589 > }
591 > private updateConflicts(): void {
592 > const conflicts = this.getActiveProfileSynchronizers().map(synchronizer => synchronizer.conflicts).flat(); userDataSyncService.ts ×4
593 > if (!equals(this._conflicts, conflicts, (a, b) => a.profile.id === b.profile.id && a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
594 > this._conflicts = conflicts;
595 > this._onDidChangeConflicts.fire(conflicts);
596 > }
597 > }
599 > private updateLastSyncTime(): void {
600 > if (this.status === SyncStatus.Idle) { extensionsSync.ts ×17
601 > this._lastSyncTime = new Date().getTime();
602 > this.storageService.store(LAST_SYNC_TIME_KEY, this._lastSyncTime, StorageScope.APPLICATION, StorageTarget.MACHINE);
603 > this._onDidChangeLastSyncTime.fire(this._lastSyncTime);
604 > }
605 > }
607 > getOrCreateActiveProfileSynchronizer(profile: IUserDataProfile, syncProfile: ISyncUserDataProfile | undefined): ProfileSynchronizer {
608 > let activeProfileSynchronizer = this.activeProfileSynchronizers.get(profile.id); userDataSyncService.ts ×19
609 > if (activeProfileSynchronizer && activeProfileSynchronizer[0].collection !== syncProfile?.collection) {
610 this.logService.error('Profile synchronizer collection does not match with the remote sync profile collection');
611 activeProfileSynchronizer[1].dispose();
612 activeProfileSynchronizer = undefined;
613 this.activeProfileSynchronizers.delete(profile.id);
614 }
615 > if (!activeProfileSynchronizer) { userDataSyncService.ts ×19
616 > const disposables = new DisposableStore();
617 > const profileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, syncProfile?.collection));
618 > disposables.add(profileSynchronizer.onDidChangeStatus(e => this.setStatus(e)));
619 > disposables.add(profileSynchronizer.onDidChangeConflicts(conflicts => this.updateConflicts()));
620 > disposables.add(profileSynchronizer.onDidChangeLocal(e => this._onDidChangeLocal.fire(e)));
621 > this.activeProfileSynchronizers.set(profile.id, activeProfileSynchronizer = [profileSynchronizer, disposables]);
622 > }
623 > return activeProfileSynchronizer[0];
624 > }
626 > private getActiveProfileSynchronizers(): ProfileSynchronizer[] {
627 > const profileSynchronizers: ProfileSynchronizer[] = []; userDataSyncService.ts ×4
628 > for (const [profileSynchronizer] of this.activeProfileSynchronizers.values()) {
629 > profileSynchronizers.push(profileSynchronizer);
630 > }
631 > return profileSynchronizers;
632 > }
634 > private clearActiveProfileSynchronizers(): void {
635 > this.activeProfileSynchronizers.forEach(([, disposable]) => disposable.dispose()); userDataSyncStoreService.ts ×36
636 > this.activeProfileSynchronizers.clear();
637 > }
639 > private checkEnablement(): void {
640 > if (!this.userDataSyncStoreManagementService.userDataSyncStore) { userDataSyncService.ts ×22
641 throw new Error('Not enabled');
642 }
645 > }
646 >
647 >
648 > class ProfileSynchronizer extends Disposable {
649 >
650 > private _enabled: [IUserDataSynchroniser, number, IDisposable][] = [];
651 > get enabled(): IUserDataSynchroniser[] { return this._enabled.sort((a, b) => a[1] - b[1]).map(([synchronizer]) => synchronizer); }
652 >
653 > get disabled(): SyncResource[] { return ALL_SYNC_RESOURCES.filter(syncResource => !this.userDataSyncEnablementService.isResourceEnabled(syncResource)); }
654 >
655 > private _status: SyncStatus = SyncStatus.Idle;
656 > get status(): SyncStatus { return this._status; }
657 > private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
658 > readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
659 >
660 > private _onDidChangeLocal = this._register(new Emitter<SyncResource>());
661 > readonly onDidChangeLocal = this._onDidChangeLocal.event;
662 >
663 > private _conflicts: IUserDataSyncResourceConflicts[] = [];
664 > get conflicts(): IUserDataSyncResourceConflicts[] { return this._conflicts; }
665 > private _onDidChangeConflicts = this._register(new Emitter<IUserDataSyncResourceConflicts[]>());
666 > readonly onDidChangeConflicts = this._onDidChangeConflicts.event;
667 >
668 > constructor(
669 > readonly profile: IUserDataProfile, userDataSyncService.ts ×19
670 > readonly collection: string | undefined,
671 > @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
672 > @IInstantiationService private readonly instantiationService: IInstantiationService,
673 > @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
674 > @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
675 > @ITelemetryService private readonly telemetryService: ITelemetryService,
676 > @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
677 > @IConfigurationService private readonly configurationService: IConfigurationService,
678 > ) {
679 > super();
680 > this._register(userDataSyncEnablementService.onDidChangeResourceEnablement(([syncResource, enablement]) => this.onDidChangeResourceEnablement(syncResource, enablement)));
681 > this._register(toDisposable(() => this._enabled.splice(0, this._enabled.length).forEach(([, , disposable]) => disposable.dispose())));
682 > for (const syncResource of ALL_SYNC_RESOURCES) {
683 > if (userDataSyncEnablementService.isResourceEnabled(syncResource)) {
684 > this.registerSynchronizer(syncResource);
685 > }
686 > }
687 > }
689 > private onDidChangeResourceEnablement(syncResource: SyncResource, enabled: boolean): void {
690 > if (enabled) { userDataSyncService.ts ×3
691 this.registerSynchronizer(syncResource);
693 > this.deRegisterSynchronizer(syncResource);
694 > }
695 > }
697 > protected registerSynchronizer(syncResource: SyncResource): void {
698 > if (this._enabled.some(([synchronizer]) => synchronizer.resource === syncResource)) { userDataSyncService.ts ×19
699 return;
700 }
701 > if (syncResource === SyncResource.Extensions && !this.extensionGalleryService.isEnabled()) { userDataSyncService.ts ×19
702 this.logService.info('Skipping extensions sync because gallery is not configured');
703 return;
704 }
705 > if (syncResource === SyncResource.Profiles) { userDataSyncService.ts ×19
706 > if (!this.profile.isDefault) {
708 > }
710 > if (syncResource === SyncResource.WorkspaceState) {
711 return;
712 }
713 > if (syncResource !== SyncResource.Profiles && this.profile.useDefaultFlags?.[syncResource]) { userDataSyncService.ts ×19
714 > this.logService.debug(`Skipping syncing ${syncResource} in ${this.profile.name} because it is already synced by default profile`); userDataSyncService.ts ×1
715 > return;
716 > }
717 > const disposables = new DisposableStore(); userDataSyncService.ts ×19
718 > const synchronizer = disposables.add(this.createSynchronizer(syncResource));
719 > disposables.add(synchronizer.onDidChangeStatus(() => this.updateStatus()));
720 > disposables.add(synchronizer.onDidChangeConflicts(() => this.updateConflicts()));
721 > disposables.add(synchronizer.onDidChangeLocal(() => this._onDidChangeLocal.fire(syncResource)));
722 > const order = this.getOrder(syncResource);
723 > this._enabled.push([synchronizer, order, disposables]);
724 > }
726 > private deRegisterSynchronizer(syncResource: SyncResource): void {
727 > const index = this._enabled.findIndex(([synchronizer]) => synchronizer.resource === syncResource); userDataSyncService.ts ×3
728 > if (index !== -1) {
729 > const [[synchronizer, , disposable]] = this._enabled.splice(index, 1);
730 > disposable.dispose();
731 > this.updateStatus();
732 > synchronizer.stop().then(null, error => this.logService.error(error));
733 > }
734 > }
736 > createSynchronizer(syncResource: Exclude<SyncResource, SyncResource.WorkspaceState>): IUserDataSynchroniser & IDisposable {
737 > switch (syncResource) { userDataSyncService.ts ×19
738 > case SyncResource.Settings: return this.instantiationService.createInstance(SettingsSynchroniser, this.profile, this.collection);
739 > case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser, this.profile, this.collection);
740 > case SyncResource.Snippets: return this.instantiationService.createInstance(SnippetsSynchroniser, this.profile, this.collection);
741 > case SyncResource.Prompts: return this.instantiationService.createInstance(PromptsSynchronizer, this.profile, this.collection);
742 > case SyncResource.Tasks: return this.instantiationService.createInstance(TasksSynchroniser, this.profile, this.collection);
743 > case SyncResource.Mcp: return this.instantiationService.createInstance(McpSynchroniser, this.profile, this.collection);
744 > case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser, this.profile, this.collection);
745 > case SyncResource.Extensions: return this.instantiationService.createInstance(ExtensionsSynchroniser, this.profile, this.collection);
746 > case SyncResource.Profiles: return this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.profile, this.collection);
747 > }
748 > }
750 > async sync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<[SyncResource, UserDataSyncError][]> {
752 > // Return if cancellation is requested
753 > if (token.isCancellationRequested) {
754 return [];
755 }
757 > const synchronizers = this.enabled;
758 > if (!synchronizers.length) {
759 return [];
760 }
762 > try {
763 > const syncErrors: [SyncResource, UserDataSyncError][] = [];
764 > const syncHeaders = createSyncHeaders(executionId);
765 > const userDataSyncConfiguration = preview ? await this.getUserDataSyncConfiguration(manifestOrLatestData) : this.getLocalUserDataSyncConfiguration();
766 > for (const synchroniser of synchronizers) {
767 > // Return if cancellation is requested
768 > if (token.isCancellationRequested) {
769 > return []; userDataSyncService.ts ×6
770 > }
772 > // Return if resource is not enabled
773 > if (!this.userDataSyncEnablementService.isResourceEnabled(synchroniser.resource)) {
774 return [];
775 }
777 > try {
778 > const refOrUserData = getRefOrUserData(manifestOrLatestData, this.collection, synchroniser.resource) ?? null;
779 > await synchroniser.sync(refOrUserData, preview, userDataSyncConfiguration, syncHeaders);
780 > } catch (e) {
781 > const userDataSyncError = UserDataSyncError.toUserDataSyncError(e); userDataSyncService.ts ×3
782 > reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
783 > if (canBailout(e)) {
784 > throw userDataSyncError;
785 > }
786
787 // Log and and continue
788 this.logService.error(e);
789 this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
790 syncErrors.push([synchroniser.resource, userDataSyncError]);
791 }
794 > return syncErrors;
795 > } finally { userDataSyncService.ts ×22
796 > this.updateStatus();
797 > }
798 > }
800 > async apply(executionId: string, token: CancellationToken): Promise<void> {
801 const syncHeaders = createSyncHeaders(executionId);
802 for (const synchroniser of this.enabled) {
803 if (token.isCancellationRequested) {
804 return;
805 }
806 try {
807 await synchroniser.apply(false, syncHeaders);
808 } catch (e) {
809 const userDataSyncError = UserDataSyncError.toUserDataSyncError(e);
810 reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService);
811 if (canBailout(e)) {
812 throw userDataSyncError;
813 }
814
815 // Log and and continue
816 this.logService.error(e);
817 this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
818 }
819 }
820 }
822 > async stop(): Promise<void> {
823 > for (const synchroniser of this.enabled) { userDataSyncService.ts ×6
824 > try {
825 > if (synchroniser.status !== SyncStatus.Idle) {
826 > await synchroniser.stop();
827 > }
828 > } catch (e) {
829 this.logService.error(e);
830 }
832 > }
834 > async resetLocal(): Promise<void> {
835 > for (const synchroniser of this.enabled) { abstractSynchronizer.ts ×2
836 > try {
837 > await synchroniser.resetLocal();
838 > } catch (e) {
839 this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
840 this.logService.error(e);
841 }
843 > }
845 > private async getUserDataSyncConfiguration(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null): Promise<IUserDataSyncConfiguration> {
846 if (!this.profile.isDefault) {
847 return {};
848 }
849 const local = this.getLocalUserDataSyncConfiguration();
850 const settingsSynchronizer = this.enabled.find(synchronizer => synchronizer instanceof SettingsSynchroniser);
851 if (settingsSynchronizer) {
852 const remote = await settingsSynchronizer.getRemoteUserDataSyncConfiguration(getRefOrUserData(manifestOrLatestData, this.collection, SyncResource.Settings) ?? null);
853 return { ...local, ...remote };
854 }
855 return local;
856 }
858 > private getLocalUserDataSyncConfiguration(): IUserDataSyncConfiguration {
859 > return this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE); userDataSyncService.ts ×22
860 > }
862 > private setStatus(status: SyncStatus): void {
863 > if (this._status !== status) { userDataSyncService.ts ×19
864 > this._status = status;
865 > this._onDidChangeStatus.fire(status);
866 > }
867 > }
869 > private updateStatus(): void {
870 > this.updateConflicts(); userDataSyncService.ts ×19
871 > if (this.enabled.some(s => s.status === SyncStatus.HasConflicts)) {
872 > return this.setStatus(SyncStatus.HasConflicts); userDataSyncService.ts ×4
873 > }
874 > if (this.enabled.some(s => s.status === SyncStatus.Syncing)) { userDataSyncService.ts ×19
875 > return this.setStatus(SyncStatus.Syncing);
876 > }
877 > return this.setStatus(SyncStatus.Idle); userDataSyncService.ts ×1
880 > private updateConflicts(): void {
881 > const conflicts = this.enabled.filter(s => s.status === SyncStatus.HasConflicts) userDataSyncService.ts ×19
882 > .filter(s => s.conflicts.conflicts.length > 0)
883 > .map(s => s.conflicts);
884 > if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
885 > this._conflicts = conflicts; userDataSyncService.ts ×4
886 > this._onDidChangeConflicts.fire(conflicts);
887 > }
890 > private getOrder(syncResource: SyncResource): number {
891 > switch (syncResource) { userDataSyncService.ts ×19
892 > case SyncResource.Settings: return 0;
893 > case SyncResource.Keybindings: return 1;
894 > case SyncResource.Snippets: return 2;
895 > case SyncResource.Tasks: return 3;
896 > case SyncResource.Mcp: return 4;
897 > case SyncResource.GlobalState: return 5;
898 > case SyncResource.Extensions: return 6;
899 > case SyncResource.Prompts: return 7;
900 > case SyncResource.Profiles: return 8;
901 > case SyncResource.WorkspaceState: return 9;
902 > }
903 > }
905 >
906 > function canBailout(e: unknown): boolean { userDataSyncService.ts ×3
907 > if (e instanceof UserDataSyncError) {
908 > switch (e.code) {
909 > case UserDataSyncErrorCode.MethodNotFound:
910 > case UserDataSyncErrorCode.TooLarge:
911 > case UserDataSyncErrorCode.TooManyRequests:
912 > case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter:
913 > case UserDataSyncErrorCode.LocalTooManyRequests:
914 > case UserDataSyncErrorCode.LocalTooManyProfiles:
915 > case UserDataSyncErrorCode.Gone:
916 > case UserDataSyncErrorCode.UpgradeRequired:
917 > case UserDataSyncErrorCode.IncompatibleRemoteContent:
918 > case UserDataSyncErrorCode.IncompatibleLocalContent:
919 > return true;
920 > }
921 > }
922 return false;
923 }
925 > function reportUserDataSyncError(userDataSyncError: UserDataSyncError, executionId: string, userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, telemetryService: ITelemetryService): void { userDataSyncService.ts ×3
926 > telemetryService.publicLog2<SyncErrorEvent, SyncErrorClassification>('sync/error',
927 > {
928 > code: userDataSyncError.code,
929 > serverCode: userDataSyncError instanceof UserDataSyncStoreError ? String(userDataSyncError.serverCode) : undefined,
930 > url: userDataSyncError instanceof UserDataSyncStoreError ? userDataSyncError.url : undefined,
931 > resource: userDataSyncError.resource,
932 > executionId,
933 > service: userDataSyncStoreManagementService.userDataSyncStore!.url.toString()
934 > });
935 > }
937 > function getRefOrUserData(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, collection: string | undefined, resource: SyncResource): string | IUserData | undefined { userDataSyncService.ts ×22
938 > if (isUserDataManifest(manifestOrLatestData)) {
939 > if (collection) { userDataSyncService.ts ×2
940 > return manifestOrLatestData?.collections?.[collection]?.latest?.[resource]; userDataSyncService.ts ×1
941 > }
942 > return manifestOrLatestData?.latest?.[resource]; userDataSyncService.ts ×2
943 > }
944 > if (collection) { userDataSyncService.ts ×2
945 > return manifestOrLatestData?.collections?.[collection]?.resources?.[resource]; userDataSyncService.ts ×1
946 > }
947 > return manifestOrLatestData?.resources?.[resource]; userDataSyncService.ts ×2