Skip to content

Commit 25e5a2a

Browse files
bpaserojoyceerhl
andauthored
storage - storeAll and external (microsoft#185837)
* Emit source in storage change event * Address some comments * Fix failed rename * Part -> Self * JSDoc for new param * Add tests * Fix tests * Switch from enum to boolean * Fix tests * first cut * adopt storeAll in more places * more adoptions * renames * more adoptions * extract interface * more renames * more tests * fix test * more tests * 💄 jsdoc --------- Co-authored-by: Joyce Er <[email protected]>
1 parent ec8c12d commit 25e5a2a

File tree

14 files changed

+183
-61
lines changed

14 files changed

+183
-61
lines changed

src/vs/base/parts/storage/common/storage.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { ThrottledDelayer } from 'vs/base/common/async';
7-
import { Emitter, Event } from 'vs/base/common/event';
7+
import { Event, PauseableEmitter } from 'vs/base/common/event';
88
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
99
import { parse, stringify } from 'vs/base/common/marshalling';
1010
import { isObject, isUndefinedOrNull } from 'vs/base/common/types';
@@ -52,9 +52,29 @@ export interface IStorageDatabase {
5252
close(recovery?: () => Map<string, string>): Promise<void>;
5353
}
5454

55+
export interface IStorageChangeEvent {
56+
57+
/**
58+
* The `key` of the storage entry that was changed
59+
* or was removed.
60+
*/
61+
readonly key: string;
62+
63+
/**
64+
* A hint how the storage change event was triggered. If
65+
* `true`, the storage change was triggered by an external
66+
* source, such as:
67+
* - another process (for example another window)
68+
* - operations such as settings sync or profiles change
69+
*/
70+
readonly external?: boolean;
71+
}
72+
73+
export type StorageValue = string | boolean | number | undefined | null | object;
74+
5575
export interface IStorage extends IDisposable {
5676

57-
readonly onDidChangeStorage: Event<string>;
77+
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
5878

5979
readonly items: Map<string, string>;
6080
readonly size: number;
@@ -73,8 +93,8 @@ export interface IStorage extends IDisposable {
7393
getObject<T extends object>(key: string, fallbackValue: T): T;
7494
getObject<T extends object>(key: string, fallbackValue?: T): T | undefined;
7595

76-
set(key: string, value: string | boolean | number | undefined | null | object): Promise<void>;
77-
delete(key: string): Promise<void>;
96+
set(key: string, value: StorageValue, external?: boolean): Promise<void>;
97+
delete(key: string, external?: boolean): Promise<void>;
7898

7999
flush(delay?: number): Promise<void>;
80100
whenFlushed(): Promise<void>;
@@ -92,7 +112,7 @@ export class Storage extends Disposable implements IStorage {
92112

93113
private static readonly DEFAULT_FLUSH_DELAY = 100;
94114

95-
private readonly _onDidChangeStorage = this._register(new Emitter<string>());
115+
private readonly _onDidChangeStorage = this._register(new PauseableEmitter<IStorageChangeEvent>());
96116
readonly onDidChangeStorage = this._onDidChangeStorage.event;
97117

98118
private state = StorageState.None;
@@ -122,14 +142,22 @@ export class Storage extends Disposable implements IStorage {
122142
}
123143

124144
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
125-
// items that change external require us to update our
126-
// caches with the values. we just accept the value and
127-
// emit an event if there is a change.
128-
e.changed?.forEach((value, key) => this.accept(key, value));
129-
e.deleted?.forEach(key => this.accept(key, undefined));
145+
this._onDidChangeStorage.pause();
146+
147+
try {
148+
// items that change external require us to update our
149+
// caches with the values. we just accept the value and
150+
// emit an event if there is a change.
151+
152+
e.changed?.forEach((value, key) => this.acceptExternal(key, value));
153+
e.deleted?.forEach(key => this.acceptExternal(key, undefined));
154+
155+
} finally {
156+
this._onDidChangeStorage.resume();
157+
}
130158
}
131159

132-
private accept(key: string, value: string | undefined): void {
160+
private acceptExternal(key: string, value: string | undefined): void {
133161
if (this.state === StorageState.Closed) {
134162
return; // Return early if we are already closed
135163
}
@@ -152,7 +180,7 @@ export class Storage extends Disposable implements IStorage {
152180

153181
// Signal to outside listeners
154182
if (changed) {
155-
this._onDidChangeStorage.fire(key);
183+
this._onDidChangeStorage.fire({ key, external: true });
156184
}
157185
}
158186

@@ -229,14 +257,14 @@ export class Storage extends Disposable implements IStorage {
229257
return parse(value);
230258
}
231259

232-
async set(key: string, value: string | boolean | number | null | undefined | object): Promise<void> {
260+
async set(key: string, value: string | boolean | number | null | undefined | object, external = false): Promise<void> {
233261
if (this.state === StorageState.Closed) {
234262
return; // Return early if we are already closed
235263
}
236264

237265
// We remove the key for undefined/null values
238266
if (isUndefinedOrNull(value)) {
239-
return this.delete(key);
267+
return this.delete(key, external);
240268
}
241269

242270
// Otherwise, convert to String and store
@@ -254,13 +282,13 @@ export class Storage extends Disposable implements IStorage {
254282
this.pendingDeletes.delete(key);
255283

256284
// Event
257-
this._onDidChangeStorage.fire(key);
285+
this._onDidChangeStorage.fire({ key, external });
258286

259287
// Accumulate work by scheduling after timeout
260288
return this.doFlush();
261289
}
262290

263-
async delete(key: string): Promise<void> {
291+
async delete(key: string, external = false): Promise<void> {
264292
if (this.state === StorageState.Closed) {
265293
return; // Return early if we are already closed
266294
}
@@ -278,7 +306,7 @@ export class Storage extends Disposable implements IStorage {
278306
this.pendingInserts.delete(key);
279307

280308
// Event
281-
this._onDidChangeStorage.fire(key);
309+
this._onDidChangeStorage.fire({ key, external });
282310

283311
// Accumulate work by scheduling after timeout
284312
return this.doFlush();

src/vs/base/parts/storage/test/node/storage.integrationTest.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ flakySuite('Storage Library', function () {
5959
deepStrictEqual(storage.getObject('foo', { 'bar': 'baz' }), { 'bar': 'baz' });
6060

6161
let changes = new Set<string>();
62-
storage.onDidChangeStorage(key => {
63-
changes.add(key);
62+
storage.onDidChangeStorage(e => {
63+
changes.add(e.key);
6464
});
6565

6666
await storage.whenFlushed(); // returns immediately when no pending updates
@@ -149,8 +149,8 @@ flakySuite('Storage Library', function () {
149149
const storage = new Storage(database);
150150

151151
const changes = new Set<string>();
152-
storage.onDidChangeStorage(key => {
153-
changes.add(key);
152+
storage.onDidChangeStorage(e => {
153+
changes.add(e.key);
154154
});
155155

156156
await storage.init();
@@ -272,8 +272,8 @@ flakySuite('Storage Library', function () {
272272
await storage.init();
273273

274274
let changes = new Set<string>();
275-
storage.onDidChangeStorage(key => {
276-
changes.add(key);
275+
storage.onDidChangeStorage(e => {
276+
changes.add(e.key);
277277
});
278278

279279
const set1Promise = storage.set('foo', 'bar1');

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ suite('FindController', async () => {
7777
getNumber: (key: string) => undefined!,
7878
getObject: (key: string) => undefined!,
7979
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
80+
storeAll: () => { throw new Error(); },
8081
remove: () => undefined,
8182
isNew: () => false,
8283
flush: () => { return Promise.resolve(); },
@@ -510,6 +511,7 @@ suite('FindController query options persistence', async () => {
510511
getNumber: (key: string) => undefined!,
511512
getObject: (key: string) => undefined!,
512513
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
514+
storeAll: () => { throw new Error(); },
513515
remove: () => undefined,
514516
isNew: () => false,
515517
flush: () => { return Promise.resolve(); },

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ suite('Multicursor selection', () => {
9494
getNumber: (key: string) => undefined!,
9595
getObject: (key: string) => undefined!,
9696
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
97+
storeAll: () => { throw new Error(); },
9798
remove: (key) => undefined,
9899
log: () => undefined,
99100
switch: () => Promise.resolve(undefined),

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

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
88
import { Disposable, dispose, MutableDisposable } from 'vs/base/common/lifecycle';
99
import { mark } from 'vs/base/common/performance';
1010
import { isUndefinedOrNull } from 'vs/base/common/types';
11-
import { InMemoryStorageDatabase, IStorage, Storage, StorageHint } from 'vs/base/parts/storage/common/storage';
11+
import { InMemoryStorageDatabase, IStorage, IStorageChangeEvent, Storage, StorageHint, StorageValue } from 'vs/base/parts/storage/common/storage';
1212
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1313
import { isUserDataProfile, IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
1414
import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
@@ -35,6 +35,13 @@ export interface IWillSaveStateEvent {
3535
readonly reason: WillSaveStateReason;
3636
}
3737

38+
export interface IStorageEntry {
39+
readonly key: string;
40+
readonly value: StorageValue;
41+
readonly scope: StorageScope;
42+
readonly target: StorageTarget;
43+
}
44+
3845
export interface IStorageService {
3946

4047
readonly _serviceBrand: undefined;
@@ -119,7 +126,16 @@ export interface IStorageService {
119126
* @param target allows to define the target of the storage operation
120127
* to either the current machine or user.
121128
*/
122-
store(key: string, value: string | boolean | number | undefined | null | object, scope: StorageScope, target: StorageTarget): void;
129+
store(key: string, value: StorageValue, scope: StorageScope, target: StorageTarget): void;
130+
131+
/**
132+
* Allows to store multiple values in a bulk operation. Events will only
133+
* be emitted when all values have been stored.
134+
*
135+
* @param external a hint to indicate the source of the operation is external,
136+
* such as settings sync or profile changes.
137+
*/
138+
storeAll(entries: Array<IStorageEntry>, external: boolean): void;
123139

124140
/**
125141
* Delete an element stored under the provided key from storage.
@@ -229,6 +245,15 @@ export interface IStorageValueChangeEvent {
229245
* removed.
230246
*/
231247
readonly target: StorageTarget | undefined;
248+
249+
/**
250+
* A hint how the storage change event was triggered. If
251+
* `true`, the storage change was triggered by an external
252+
* source, such as:
253+
* - another process (for example another window)
254+
* - operations such as settings sync or profiles change
255+
*/
256+
readonly external?: boolean;
232257
}
233258

234259
export interface IStorageTargetChangeEvent {
@@ -332,7 +357,8 @@ export abstract class AbstractStorageService extends Disposable implements IStor
332357
return this.initializationPromise;
333358
}
334359

335-
protected emitDidChangeValue(scope: StorageScope, key: string): void {
360+
protected emitDidChangeValue(scope: StorageScope, event: IStorageChangeEvent): void {
361+
const { key, external } = event;
336362

337363
// Specially handle `TARGET_KEY`
338364
if (key === TARGET_KEY) {
@@ -356,7 +382,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
356382

357383
// Emit any other key to outside
358384
else {
359-
this._onDidChangeValue.fire({ scope, key, target: this.getKeyTargets(scope)[key] });
385+
this._onDidChangeValue.fire({ scope, key, target: this.getKeyTargets(scope)[key], external });
360386
}
361387
}
362388

@@ -388,11 +414,19 @@ export abstract class AbstractStorageService extends Disposable implements IStor
388414
return this.getStorage(scope)?.getObject(key, fallbackValue);
389415
}
390416

391-
store(key: string, value: string | boolean | number | undefined | null | object, scope: StorageScope, target: StorageTarget): void {
417+
storeAll(entries: Array<IStorageEntry>, external: boolean): void {
418+
this.withPausedEmitters(() => {
419+
for (const entry of entries) {
420+
this.store(entry.key, entry.value, entry.scope, entry.target, external);
421+
}
422+
});
423+
}
424+
425+
store(key: string, value: StorageValue, scope: StorageScope, target: StorageTarget, external = false): void {
392426

393427
// We remove the key for undefined/null values
394428
if (isUndefinedOrNull(value)) {
395-
this.remove(key, scope);
429+
this.remove(key, scope, external);
396430
return;
397431
}
398432

@@ -403,11 +437,11 @@ export abstract class AbstractStorageService extends Disposable implements IStor
403437
this.updateKeyTarget(key, scope, target);
404438

405439
// Store actual value
406-
this.getStorage(scope)?.set(key, value);
440+
this.getStorage(scope)?.set(key, value, external);
407441
});
408442
}
409443

410-
remove(key: string, scope: StorageScope): void {
444+
remove(key: string, scope: StorageScope, external = false): void {
411445

412446
// Update our datastructures but send events only after
413447
this.withPausedEmitters(() => {
@@ -416,7 +450,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
416450
this.updateKeyTarget(key, scope, undefined);
417451

418452
// Remove actual key
419-
this.getStorage(scope)?.delete(key);
453+
this.getStorage(scope)?.delete(key, external);
420454
});
421455
}
422456

@@ -450,22 +484,22 @@ export abstract class AbstractStorageService extends Disposable implements IStor
450484
return keys;
451485
}
452486

453-
private updateKeyTarget(key: string, scope: StorageScope, target: StorageTarget | undefined): void {
487+
private updateKeyTarget(key: string, scope: StorageScope, target: StorageTarget | undefined, external = false): void {
454488

455489
// Add
456490
const keyTargets = this.getKeyTargets(scope);
457491
if (typeof target === 'number') {
458492
if (keyTargets[key] !== target) {
459493
keyTargets[key] = target;
460-
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets));
494+
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets), external);
461495
}
462496
}
463497

464498
// Remove
465499
else {
466500
if (typeof keyTargets[key] === 'number') {
467501
delete keyTargets[key];
468-
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets));
502+
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets), external);
469503
}
470504
}
471505
}
@@ -595,7 +629,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
595629
// Copy over previous keys if `preserveData`
596630
if (preserveData) {
597631
for (const [key, value] of oldStorage) {
598-
newStorage.set(key, value);
632+
newStorage.set(key, value, true);
599633
}
600634
}
601635

@@ -607,13 +641,13 @@ export abstract class AbstractStorageService extends Disposable implements IStor
607641

608642
const newValue = newStorage.get(key);
609643
if (newValue !== oldValue) {
610-
this.emitDidChangeValue(scope, key);
644+
this.emitDidChangeValue(scope, { key, external: true });
611645
}
612646
}
613647

614648
for (const [key] of newStorage.items) {
615649
if (!handledkeys.has(key)) {
616-
this.emitDidChangeValue(scope, key);
650+
this.emitDidChangeValue(scope, { key, external: true });
617651
}
618652
}
619653
}
@@ -647,9 +681,9 @@ export class InMemoryStorageService extends AbstractStorageService {
647681
constructor() {
648682
super();
649683

650-
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
651-
this._register(this.profileStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.PROFILE, key)));
652-
this._register(this.applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key)));
684+
this._register(this.workspaceStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.WORKSPACE, e)));
685+
this._register(this.profileStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.PROFILE, e)));
686+
this._register(this.applicationStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.APPLICATION, e)));
653687
}
654688

655689
protected getStorage(scope: StorageScope): IStorage {

0 commit comments

Comments
 (0)