Skip to content

Commit 5352ba3

Browse files
author
Benjamin Pasero
committed
web - synchronise global state changes
1 parent 73f852d commit 5352ba3

3 files changed

Lines changed: 143 additions & 100 deletions

File tree

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

Lines changed: 138 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55

66
import { Disposable } from 'vs/base/common/lifecycle';
77
import { Event, Emitter } from 'vs/base/common/event';
8-
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, FileStorageDatabase } from 'vs/platform/storage/common/storage';
8+
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
99
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
1010
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
1111
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
12-
import { IFileService } from 'vs/platform/files/common/files';
13-
import { IStorage, Storage } from 'vs/base/parts/storage/common/storage';
12+
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
13+
import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
1414
import { URI } from 'vs/base/common/uri';
1515
import { joinPath } from 'vs/base/common/resources';
16-
import { runWhenIdle } from 'vs/base/common/async';
16+
import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
17+
import { serializableToMap, mapToSerializable } from 'vs/base/common/map';
18+
import { VSBuffer } from 'vs/base/common/buffer';
1719

1820
export class BrowserStorageService extends Disposable implements IStorageService {
1921

@@ -35,6 +37,7 @@ export class BrowserStorageService extends Disposable implements IStorageService
3537
private workspaceStorageFile: URI;
3638

3739
private initializePromise: Promise<void>;
40+
private periodicSaveScheduler = this._register(new RunOnceScheduler(() => this.saveState(), 5000));
3841

3942
get hasPendingUpdate(): boolean {
4043
return this.globalStorageDatabase.hasPendingUpdate || this.workspaceStorageDatabase.hasPendingUpdate;
@@ -51,20 +54,23 @@ export class BrowserStorageService extends Disposable implements IStorageService
5154
// long running operation.
5255
// Instead, periodically ask customers to save save. The library will be clever enough
5356
// to only save state that has actually changed.
54-
this.saveStatePeriodically();
57+
this.periodicSaveScheduler.schedule();
5558
}
5659

57-
private saveStatePeriodically(): void {
58-
setTimeout(() => {
59-
runWhenIdle(() => {
60+
private saveState(): void {
61+
runWhenIdle(() => {
6062

61-
// this event will potentially cause new state to be stored
63+
// this event will potentially cause new state to be stored
64+
// since new state will only be created while the document
65+
// has focus, one optimization is to not run this when the
66+
// document has no focus, assuming that state has not changed
67+
if (document.hasFocus()) {
6268
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
69+
}
6370

64-
// repeat
65-
this.saveStatePeriodically();
66-
});
67-
}, 5000);
71+
// repeat
72+
this.periodicSaveScheduler.schedule();
73+
});
6874
}
6975

7076
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
@@ -83,14 +89,14 @@ export class BrowserStorageService extends Disposable implements IStorageService
8389

8490
// Workspace Storage
8591
this.workspaceStorageFile = joinPath(stateRoot, `${payload.id}.json`);
86-
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, this.fileService));
87-
this.workspaceStorage = new Storage(this.workspaceStorageDatabase);
92+
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
93+
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
8894
this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE })));
8995

9096
// Global Storage
9197
this.globalStorageFile = joinPath(stateRoot, 'global.json');
92-
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, this.fileService));
93-
this.globalStorage = new Storage(this.globalStorageDatabase);
98+
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
99+
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
94100
this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL })));
95101

96102
// Init both
@@ -140,14 +146,125 @@ export class BrowserStorageService extends Disposable implements IStorageService
140146
}
141147

142148
close(): void {
143-
144-
// Signal as event so that clients can still store data
145-
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
146-
147149
// We explicitly do not close our DBs because writing data onBeforeUnload()
148150
// can result in unexpected results. Namely, it seems that - even though this
149151
// operation is async - sometimes it is being triggered on unload and
150152
// succeeds. Often though, the DBs turn out to be empty because the write
151153
// never had a chance to complete.
154+
//
155+
// Instead we trigger dispose() to ensure that no timeouts or callbacks
156+
// get triggered in this phase.
157+
this.dispose();
158+
}
159+
}
160+
161+
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
162+
163+
private readonly _onDidChangeItemsExternal: Emitter<IStorageItemsChangeEvent> = this._register(new Emitter<IStorageItemsChangeEvent>());
164+
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent> = this._onDidChangeItemsExternal.event;
165+
166+
private cache: Map<string, string> | undefined;
167+
168+
private pendingUpdate: Promise<void> = Promise.resolve();
169+
170+
private _hasPendingUpdate = false;
171+
get hasPendingUpdate(): boolean {
172+
return this._hasPendingUpdate;
173+
}
174+
175+
private isWatching = false;
176+
177+
constructor(
178+
private readonly file: URI,
179+
private readonly watchForExternalChanges: boolean,
180+
@IFileService private readonly fileService: IFileService
181+
) {
182+
super();
183+
}
184+
185+
private async ensureWatching(): Promise<void> {
186+
if (this.isWatching || !this.watchForExternalChanges) {
187+
return;
188+
}
189+
190+
const exists = await this.fileService.exists(this.file);
191+
if (this.isWatching || !exists) {
192+
return; // file must exist to be watched
193+
}
194+
195+
this.isWatching = true;
196+
197+
this._register(this.fileService.watch(this.file));
198+
this._register(this.fileService.onFileChanges(e => {
199+
if (document.hasFocus()) {
200+
return; // ignore changes from ourselves by checking for focus
201+
}
202+
203+
if (!e.contains(this.file, FileChangeType.UPDATED)) {
204+
return; // not our file
205+
}
206+
207+
this.onDidStorageChangeExternal();
208+
}));
209+
}
210+
211+
private async onDidStorageChangeExternal(): Promise<void> {
212+
const items = await this.doGetItemsFromFile();
213+
214+
this.cache = items;
215+
216+
this._onDidChangeItemsExternal.fire({ items });
217+
}
218+
219+
async getItems(): Promise<Map<string, string>> {
220+
if (!this.cache) {
221+
try {
222+
this.cache = await this.doGetItemsFromFile();
223+
} catch (error) {
224+
this.cache = new Map();
225+
}
226+
}
227+
228+
return this.cache;
229+
}
230+
231+
private async doGetItemsFromFile(): Promise<Map<string, string>> {
232+
await this.pendingUpdate;
233+
234+
const itemsRaw = await this.fileService.readFile(this.file);
235+
236+
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
237+
238+
return serializableToMap(JSON.parse(itemsRaw.value.toString()));
239+
}
240+
241+
async updateItems(request: IUpdateRequest): Promise<void> {
242+
const items = await this.getItems();
243+
244+
if (request.insert) {
245+
request.insert.forEach((value, key) => items.set(key, value));
246+
}
247+
248+
if (request.delete) {
249+
request.delete.forEach(key => items.delete(key));
250+
}
251+
252+
await this.pendingUpdate;
253+
254+
this._hasPendingUpdate = true;
255+
256+
this.pendingUpdate = this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items))))
257+
.then(() => {
258+
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
259+
})
260+
.finally(() => {
261+
this._hasPendingUpdate = false;
262+
});
263+
264+
return this.pendingUpdate;
265+
}
266+
267+
close(): Promise<void> {
268+
return this.pendingUpdate;
152269
}
153270
}

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

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/co
77
import { Event, Emitter } from 'vs/base/common/event';
88
import { Disposable } from 'vs/base/common/lifecycle';
99
import { isUndefinedOrNull } from 'vs/base/common/types';
10-
import { IUpdateRequest, IStorageDatabase } from 'vs/base/parts/storage/common/storage';
11-
import { serializableToMap, mapToSerializable } from 'vs/base/common/map';
12-
import { VSBuffer } from 'vs/base/common/buffer';
13-
import { URI } from 'vs/base/common/uri';
14-
import { IFileService } from 'vs/platform/files/common/files';
1510

1611
export const IStorageService = createDecorator<IStorageService>('storageService');
1712

@@ -212,75 +207,6 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
212207
}
213208
}
214209

215-
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
216-
217-
readonly onDidChangeItemsExternal = Event.None; // TODO@Ben implement global UI storage events
218-
219-
private cache: Map<string, string> | undefined;
220-
221-
private pendingUpdate: Promise<void> = Promise.resolve();
222-
223-
private _hasPendingUpdate = false;
224-
get hasPendingUpdate(): boolean {
225-
return this._hasPendingUpdate;
226-
}
227-
228-
constructor(
229-
private readonly file: URI,
230-
private readonly fileService: IFileService
231-
) {
232-
super();
233-
}
234-
235-
async getItems(): Promise<Map<string, string>> {
236-
if (!this.cache) {
237-
try {
238-
this.cache = await this.doGetItemsFromFile();
239-
} catch (error) {
240-
this.cache = new Map();
241-
}
242-
}
243-
244-
return this.cache;
245-
}
246-
247-
private async doGetItemsFromFile(): Promise<Map<string, string>> {
248-
await this.pendingUpdate;
249-
250-
const itemsRaw = await this.fileService.readFile(this.file);
251-
252-
return serializableToMap(JSON.parse(itemsRaw.value.toString()));
253-
}
254-
255-
async updateItems(request: IUpdateRequest): Promise<void> {
256-
const items = await this.getItems();
257-
258-
if (request.insert) {
259-
request.insert.forEach((value, key) => items.set(key, value));
260-
}
261-
262-
if (request.delete) {
263-
request.delete.forEach(key => items.delete(key));
264-
}
265-
266-
await this.pendingUpdate;
267-
268-
this._hasPendingUpdate = true;
269-
270-
this.pendingUpdate = this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items))))
271-
.then(() => undefined)
272-
.finally(() => {
273-
this._hasPendingUpdate = false;
274-
});
275-
276-
return this.pendingUpdate;
277-
}
278-
279-
close(): Promise<void> {
280-
return this.pendingUpdate;
281-
}
282-
}
283-
284210
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {
285211
const safeParse = (value: string) => {
286212
try {

src/vs/platform/storage/test/node/storage.test.ts renamed to src/vs/platform/storage/test/electron-browser/storage.test.ts

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

66
import { equal } from 'assert';
7-
import { FileStorageDatabase } from 'vs/platform/storage/common/storage';
7+
import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
88
import { generateUuid } from 'vs/base/common/uuid';
99
import { join } from 'vs/base/common/path';
1010
import { tmpdir } from 'os';
@@ -49,7 +49,7 @@ suite('Storage', () => {
4949
});
5050

5151
test('File Based Storage', async () => {
52-
let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), fileService));
52+
let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
5353

5454
await storage.init();
5555

@@ -63,7 +63,7 @@ suite('Storage', () => {
6363

6464
await storage.close();
6565

66-
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), fileService));
66+
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
6767

6868
await storage.init();
6969

@@ -81,12 +81,12 @@ suite('Storage', () => {
8181

8282
await storage.close();
8383

84-
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), fileService));
84+
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
8585

8686
await storage.init();
8787

8888
equal(storage.get('bar', 'undefined'), 'undefined');
8989
equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
9090
equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
9191
});
92-
});
92+
});

0 commit comments

Comments
 (0)