55
66import { Disposable } from 'vs/base/common/lifecycle' ;
77import { 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' ;
99import { IEnvironmentService } from 'vs/platform/environment/common/environment' ;
1010import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces' ;
1111import { 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' ;
1414import { URI } from 'vs/base/common/uri' ;
1515import { 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
1820export 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}
0 commit comments