Skip to content

Commit 761f8b2

Browse files
authored
storage - allow to migrate to a new profile (microsoft#152317)
* storage - allow to migrate to a new profile * reuse data migration code * use correct profile for logging * cleanup * address feedback * dont dipsose store * lipstick * align * simplify * rensmes * pause emitters * reuse
1 parent 47f4175 commit 761f8b2

File tree

13 files changed

+263
-159
lines changed

13 files changed

+263
-159
lines changed

src/vs/editor/contrib/find/test/browser/findController.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ suite('FindController', async () => {
8080
isNew: () => false,
8181
flush: () => { return Promise.resolve(); },
8282
keys: () => [],
83-
logStorage: () => { },
84-
migrate: () => { throw new Error(); }
83+
log: () => { },
84+
switch: () => { throw new Error(); }
8585
} as IStorageService);
8686

8787
if (platform.isMacintosh) {
@@ -511,8 +511,8 @@ suite('FindController query options persistence', async () => {
511511
isNew: () => false,
512512
flush: () => { return Promise.resolve(); },
513513
keys: () => [],
514-
logStorage: () => { },
515-
migrate: () => { throw new Error(); }
514+
log: () => { },
515+
switch: () => { throw new Error(); }
516516
} as IStorageService);
517517

518518
test('matchCase', async () => {

src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ suite('Multicursor selection', () => {
9494
getNumber: (key: string) => undefined!,
9595
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
9696
remove: (key) => undefined,
97-
logStorage: () => undefined,
98-
migrate: (toWorkspace) => Promise.resolve(undefined),
97+
log: () => undefined,
98+
switch: () => Promise.resolve(undefined),
9999
flush: () => Promise.resolve(undefined),
100100
isNew: () => true,
101101
keys: () => []

src/vs/platform/storage/browser/storageService.ts

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,31 @@
55

66
import { isSafari } from 'vs/base/browser/browser';
77
import { IndexedDB } from 'vs/base/browser/indexedDB';
8-
import { Promises } from 'vs/base/common/async';
8+
import { DeferredPromise, Promises } from 'vs/base/common/async';
99
import { toErrorMessage } from 'vs/base/common/errorMessage';
1010
import { Emitter } from 'vs/base/common/event';
11-
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
11+
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
12+
import { assertIsDefined } from 'vs/base/common/types';
1213
import { InMemoryStorageDatabase, isStorageItemsChangeEvent, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage';
1314
import { ILogService } from 'vs/platform/log/common/log';
1415
import { AbstractStorageService, IS_NEW_KEY, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
15-
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
16+
import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
1617
import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
1718

1819
export class BrowserStorageService extends AbstractStorageService {
1920

2021
private static BROWSER_DEFAULT_FLUSH_INTERVAL = 5 * 1000; // every 5s because async operations are not permitted on shutdown
2122

2223
private applicationStorage: IStorage | undefined;
23-
private globalStorage: IStorage | undefined;
24-
private workspaceStorage: IStorage | undefined;
25-
2624
private applicationStorageDatabase: IIndexedDBStorageDatabase | undefined;
25+
private readonly applicationStoragePromise = new DeferredPromise<{ indededDb: IIndexedDBStorageDatabase; storage: IStorage }>();
26+
27+
private globalStorage: IStorage | undefined;
2728
private globalStorageDatabase: IIndexedDBStorageDatabase | undefined;
29+
private globalStorageProfile: IUserDataProfile;
30+
private readonly globalStorageDisposables = this._register(new DisposableStore());
31+
32+
private workspaceStorage: IStorage | undefined;
2833
private workspaceStorageDatabase: IIndexedDBStorageDatabase | undefined;
2934

3035
get hasPendingUpdate(): boolean {
@@ -38,20 +43,22 @@ export class BrowserStorageService extends AbstractStorageService {
3843
constructor(
3944
private readonly payload: IAnyWorkspaceIdentifier,
4045
@ILogService private readonly logService: ILogService,
41-
@IUserDataProfilesService private readonly userDataProfileService: IUserDataProfilesService
46+
@IUserDataProfilesService userDataProfileService: IUserDataProfilesService
4247
) {
4348
super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL });
49+
50+
this.globalStorageProfile = userDataProfileService.currentProfile;
4451
}
4552

4653
private getId(scope: StorageScope): string {
4754
switch (scope) {
4855
case StorageScope.APPLICATION:
4956
return 'global'; // use the default profile global DB for application scope
5057
case StorageScope.GLOBAL:
51-
if (this.userDataProfileService.currentProfile.isDefault) {
58+
if (this.globalStorageProfile.isDefault) {
5259
return 'global'; // default profile DB has a fixed name for backwards compatibility
5360
} else {
54-
return `global-${this.userDataProfileService.currentProfile.id}`;
61+
return `global-${this.globalStorageProfile.id}`;
5562
}
5663
case StorageScope.WORKSPACE:
5764
return this.payload.id;
@@ -60,55 +67,81 @@ export class BrowserStorageService extends AbstractStorageService {
6067

6168
protected async doInitialize(): Promise<void> {
6269

63-
// Create Storage in Parallel
64-
const promises: Promise<IIndexedDBStorageDatabase>[] = [];
65-
promises.push(IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.APPLICATION), broadcastChanges: true }, this.logService));
66-
promises.push(IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService));
67-
if (!this.userDataProfileService.currentProfile.isDefault) {
70+
// Init storages
71+
await Promises.settled([
72+
this.createApplicationStorage(),
73+
this.createGlobalStorage(this.globalStorageProfile),
74+
this.createWorkspaceStorage()
75+
]);
76+
}
77+
78+
private async createApplicationStorage(): Promise<void> {
79+
const applicationStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.APPLICATION), broadcastChanges: true }, this.logService);
80+
81+
this.applicationStorageDatabase = this._register(applicationStorageIndexedDB);
82+
this.applicationStorage = this._register(new Storage(this.applicationStorageDatabase));
83+
84+
this._register(this.applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key)));
85+
86+
await this.applicationStorage.init();
87+
88+
this.updateIsNew(this.applicationStorage);
89+
90+
this.applicationStoragePromise.complete({ indededDb: applicationStorageIndexedDB, storage: this.applicationStorage });
91+
}
92+
93+
private async createGlobalStorage(profile: IUserDataProfile): Promise<void> {
94+
95+
// First clear any previously associated disposables
96+
this.globalStorageDisposables.clear();
97+
98+
// Remember profile associated to global storage
99+
this.globalStorageProfile = profile;
100+
101+
if (this.globalStorageProfile.isDefault) {
68102

69103
// If we are in default profile, the global storage is
70104
// actually the same as application storage. As such we
71105
// avoid creating the storage library a second time on
72106
// the same DB.
73107

74-
promises.push(IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.GLOBAL), broadcastChanges: true }, this.logService));
108+
const { indededDb: applicationStorageIndexedDB, storage: applicationStorage } = await this.applicationStoragePromise.p;
109+
110+
this.globalStorageDatabase = applicationStorageIndexedDB;
111+
this.globalStorage = applicationStorage;
112+
} else {
113+
const globalStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.GLOBAL), broadcastChanges: true }, this.logService);
114+
115+
this.globalStorageDatabase = this.globalStorageDisposables.add(globalStorageIndexedDB);
116+
this.globalStorage = this.globalStorageDisposables.add(new Storage(this.globalStorageDatabase));
75117
}
76-
const [applicationStorageDatabase, workspaceStorageDatabase, globalStorageDatabase] = await Promises.settled(promises);
77118

78-
// Workspace Storage
79-
this.workspaceStorageDatabase = this._register(workspaceStorageDatabase);
119+
this.globalStorageDisposables.add(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
120+
121+
await this.globalStorage.init();
122+
123+
this.updateIsNew(this.globalStorage);
124+
}
125+
126+
private async createWorkspaceStorage(): Promise<void> {
127+
const workspaceStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService);
128+
129+
this.workspaceStorageDatabase = this._register(workspaceStorageIndexedDB);
80130
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
131+
81132
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
82133

83-
// Application Storage
84-
this.applicationStorageDatabase = this._register(applicationStorageDatabase);
85-
this.applicationStorage = this._register(new Storage(this.applicationStorageDatabase));
86-
this._register(this.applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key)));
134+
await this.workspaceStorage.init();
87135

88-
// Global Storage
89-
if (globalStorageDatabase) {
90-
this.globalStorageDatabase = this._register(globalStorageDatabase);
91-
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
92-
} else {
93-
this.globalStorage = this.applicationStorage;
94-
}
95-
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
96-
97-
// Init storages
98-
await Promises.settled([
99-
this.workspaceStorage.init(),
100-
this.globalStorage.init(),
101-
this.applicationStorage.init()
102-
]);
136+
this.updateIsNew(this.workspaceStorage);
137+
}
103138

104-
// Apply is-new markers
105-
for (const storage of [this.applicationStorage, this.globalStorage, this.workspaceStorage]) {
106-
const firstOpen = storage.getBoolean(IS_NEW_KEY);
107-
if (firstOpen === undefined) {
108-
storage.set(IS_NEW_KEY, true);
109-
} else if (firstOpen) {
110-
storage.set(IS_NEW_KEY, false);
111-
}
139+
private updateIsNew(storage: IStorage): void {
140+
const firstOpen = storage.getBoolean(IS_NEW_KEY);
141+
if (firstOpen === undefined) {
142+
storage.set(IS_NEW_KEY, true);
143+
} else if (firstOpen) {
144+
storage.set(IS_NEW_KEY, false);
112145
}
113146
}
114147

@@ -127,7 +160,24 @@ export class BrowserStorageService extends AbstractStorageService {
127160
return this.getId(scope);
128161
}
129162

130-
async migrate(toWorkspace: IAnyWorkspaceIdentifier): Promise<void> {
163+
protected async switchToProfile(toProfile: IUserDataProfile, preserveData: boolean): Promise<void> {
164+
const oldGlobalStorage = assertIsDefined(this.globalStorage);
165+
const oldItems = oldGlobalStorage.items;
166+
167+
// Close old global storage but only if this is
168+
// different from application storage!
169+
if (oldGlobalStorage !== this.applicationStorage) {
170+
await oldGlobalStorage.close();
171+
}
172+
173+
// Create new global storage & init
174+
await this.createGlobalStorage(toProfile);
175+
176+
// Handle data switch and eventing
177+
this.switchData(oldItems, assertIsDefined(this.globalStorage), StorageScope.GLOBAL, preserveData);
178+
}
179+
180+
protected async switchToWorkspace(toWorkspace: IAnyWorkspaceIdentifier, preserveData: boolean): Promise<void> {
131181
throw new Error('Migrating storage is currently unsupported in Web');
132182
}
133183

src/vs/platform/storage/common/storage.ts

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { mark } from 'vs/base/common/performance';
1010
import { isUndefinedOrNull } from 'vs/base/common/types';
1111
import { InMemoryStorageDatabase, IStorage, Storage } from 'vs/base/parts/storage/common/storage';
1212
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
13+
import { isUserDataProfile, IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
1314
import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
1415

1516
export const IS_NEW_KEY = '__$__isNewStorageMarker';
@@ -137,12 +138,13 @@ export interface IStorageService {
137138
/**
138139
* Log the contents of the storage to the console.
139140
*/
140-
logStorage(): void;
141+
log(): void;
141142

142143
/**
143-
* Migrate the storage contents to another workspace.
144+
* Switch storage to another workspace or profile. Optionally preserve the
145+
* current data to the new storage.
144146
*/
145-
migrate(toWorkspace: IAnyWorkspaceIdentifier): Promise<void>;
147+
switch(to: IAnyWorkspaceIdentifier | IUserDataProfile, preserveData: boolean): Promise<void>;
146148

147149
/**
148150
* Whether the storage for the given scope was created during this session or
@@ -521,7 +523,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
521523
}
522524
}
523525

524-
async logStorage(): Promise<void> {
526+
async log(): Promise<void> {
525527
const applicationItems = this.getStorage(StorageScope.APPLICATION)?.items ?? new Map<string, string>();
526528
const globalItems = this.getStorage(StorageScope.GLOBAL)?.items ?? new Map<string, string>();
527529
const workspaceItems = this.getStorage(StorageScope.WORKSPACE)?.items ?? new Map<string, string>();
@@ -536,6 +538,49 @@ export abstract class AbstractStorageService extends Disposable implements IStor
536538
);
537539
}
538540

541+
async switch(to: IAnyWorkspaceIdentifier | IUserDataProfile, preserveData: boolean): Promise<void> {
542+
543+
// Signal as event so that clients can store data before we switch
544+
this.emitWillSaveState(WillSaveStateReason.NONE);
545+
546+
if (isUserDataProfile(to)) {
547+
return this.switchToProfile(to, preserveData);
548+
}
549+
550+
return this.switchToWorkspace(to, preserveData);
551+
}
552+
553+
protected switchData(oldStorage: Map<string, string>, newStorage: IStorage, scope: StorageScope, preserveData: boolean): void {
554+
this.withPausedEmitters(() => {
555+
556+
// Copy over previous keys if `preserveData`
557+
if (preserveData) {
558+
for (const [key, value] of oldStorage) {
559+
newStorage.set(key, value);
560+
}
561+
}
562+
563+
// Otherwise signal storage keys that have changed
564+
else {
565+
const handledkeys = new Set<string>();
566+
for (const [key, oldValue] of oldStorage) {
567+
handledkeys.add(key);
568+
569+
const newValue = newStorage.get(key);
570+
if (newValue !== oldValue) {
571+
this.emitDidChangeValue(scope, key);
572+
}
573+
}
574+
575+
for (const [key] of newStorage.items) {
576+
if (!handledkeys.has(key)) {
577+
this.emitDidChangeValue(scope, key);
578+
}
579+
}
580+
}
581+
});
582+
}
583+
539584
// --- abstract
540585

541586
protected abstract doInitialize(): Promise<void>;
@@ -544,7 +589,8 @@ export abstract class AbstractStorageService extends Disposable implements IStor
544589

545590
protected abstract getLogDetails(scope: StorageScope): string | undefined;
546591

547-
abstract migrate(toWorkspace: IAnyWorkspaceIdentifier): Promise<void>;
592+
protected abstract switchToProfile(toProfile: IUserDataProfile, preserveData: boolean): Promise<void>;
593+
protected abstract switchToWorkspace(toWorkspace: IAnyWorkspaceIdentifier | IUserDataProfile, preserveData: boolean): Promise<void>;
548594
}
549595

550596
export class InMemoryStorageService extends AbstractStorageService {
@@ -585,8 +631,12 @@ export class InMemoryStorageService extends AbstractStorageService {
585631

586632
protected async doInitialize(): Promise<void> { }
587633

588-
async migrate(toWorkspace: IAnyWorkspaceIdentifier): Promise<void> {
589-
// not supported
634+
protected async switchToProfile(): Promise<void> {
635+
// no-op when in-memory
636+
}
637+
638+
protected async switchToWorkspace(): Promise<void> {
639+
// no-op when in-memory
590640
}
591641
}
592642

0 commit comments

Comments
 (0)